discovery-media-player 0.1.41 → 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.41",
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 j=Object.defineProperty;var Me=Object.getOwnPropertyDescriptor;var xe=Object.getOwnPropertyNames;var we=Object.prototype.hasOwnProperty;var y=(e,n)=>{for(var t in n)j(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&&j(e,o,{get:()=>n[o],enumerable:!(r=Me(n,o))||r.enumerable});return e};var _e=e=>Te(j({},\"__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:()=>K});var G=\"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 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(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(K(e),\"*\")}catch(t){}}function Re(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(K(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,_=r(),k=!1,U=!1,A=[],ve=()=>!!c||!!d,te=i=>{if(ve())try{b(i)}catch(l){}},W=()=>!!t&&t.visibilityState===\"visible\"&&!k,re=i=>{let l=r()-i,x=Math.max(0,_-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=()=>{_=r(),k&&(k=!1,F())},se=(i,l)=>{d||te({slug:c,event:i,page:l||M,maxPage:u,seconds:z(),sessionId:m})},R=()=>{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)},E=(i,l,x,ue)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(l,x,ue),A.push(()=>{i.removeEventListener(l,x,ue)}))};return{sessionId:m,totalSeconds:z,maxPage:()=>u,pageTimes:ie,start(){if(U)return;U=!0,se(\"open\",1),h=1,_=r(),g=W()?r():null,E(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(oe(),R()):(S(),F())}),E(n,\"focus\",()=>{S(),F()});for(let x of De)E(n,x,S,{passive:!0});E(e.scrollElement,\"scroll\",S,{passive:!0}),E(n,\"beforeunload\",R);let i=n.setInterval(()=>{!k&&r()-_>s&&(k=!0,oe())},5e3),l=n.setInterval(R,a);A.push(()=>{n.clearInterval(i),n.clearInterval(l)})},setPage(i){!i||i===M||(_=r(),V(),h=i,M=i,i>u&&(u=i,se(\"page\",i)))},setPageCount(i){v=i||0},noteActivity:S,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:()=>B,canModerate:()=>q,createScheduler:()=>Xe,escapeHtml:()=>p,flattenPresence:()=>Fe,formatMessageBody:()=>Z,initials:()=>me,isMentioned:()=>Ze,isMine:()=>w,reactorId:()=>Y,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 B(e,n){return e?`<img src=\"${p(e)}\" alt=\"\">`:p(me(n))}function Z(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 Y(e){return String(e&&(e.email||e.name)||\"\").toLowerCase()}function w(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 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}`)&&!w(e,n):!1}function Ye(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||w(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=Y(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=w(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 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>${Z(e.body)}${r}</div>`:\"\",s=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${B(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\"+(w(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var N={};y(N,{DEFAULT_CENTER:()=>Q,DEFAULT_ZOOM:()=>ee,MAP_TYPES:()=>P,cycleMapType:()=>en,initialMapContent:()=>tn,mapTypeLabel:()=>nn,sanitizeContent:()=>ne});var P=[\"roadmap\",\"satellite\",\"hybrid\"],Q=[46.6,2.5],ee=6;function T(e){let n=Number(e);return Number.isFinite(n)?n:null}function X(e){if(!Array.isArray(e))return null;let n=T(e[0]),t=T(e[1]);return n!=null&&t!=null?[n,t]:null}var J=(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:T(r.heading)||0,pitch:J(T(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:J(T(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:X(n.center)||[...Q],zoom:J(Math.trunc(T(n.zoom)||ee),1,21),marker:X(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:[...Q],zoom:ee,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=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 hn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return _e(vn);})();", SOURCE_HASH: "48e673113c31267d" };
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
@@ -1371,6 +1371,25 @@ const CLE_INVITE = "dmp-present-me";
1371
1371
  *
1372
1372
  * Signalé par la seconde passe d'audit (P1-6).
1373
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
+
1374
1393
  async function profilDuJeton(req) {
1375
1394
  try {
1376
1395
  const u = await PLAYER.identity.verifyToken((req.headers && req.headers.authorization) || "");
@@ -2042,12 +2061,52 @@ ${LEGAL_CSS}
2042
2061
  //
2043
2062
  // sendBeacon ne s'attend pas, mais il rend la main une fois la requête MISE EN FILE : signaler
2044
2063
  // juste après, puis couper, respecte l'ordre autant que ce transport le permet.
2045
- function endPresent(){ if(!PRES)return; var p=PRES; PRES=null; clearInterval(_hbIv); clearCtl(p.slug);
2046
- var pb=document.getElementById('pbar'); if(pb)pb.style.display='none';
2047
- try{ var b=JSON.stringify({action:'present-end',slug:p.slug,control:p.control});
2048
- if(navigator.sendBeacon){navigator.sendBeacon('/api/doc',new Blob([b],{type:'application/json'}));}
2049
- else {fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:b,keepalive:true});} }catch(e){}
2050
- 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
+ });
2051
2110
  // Transfert : je passe la main → je cesse de piloter SANS clôturer la présentation (le nouvel owner reprendra).
2052
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){} }
2053
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){} }
@@ -2077,14 +2136,17 @@ ${LEGAL_CSS}
2077
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); } })();
2078
2137
  }).catch(function(){});
2079
2138
  }
2080
- // Carte live : persiste le contenu (present-content, JWT) → l'audience bascule/suit via Realtime.
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.
2081
2143
  // ⚠️ REND SA PROMESSE, ET CE N'EST PAS DÉCORATIF. L'ordonnanceur des écritures de carte
2082
2144
  // appelait fini() juste après l'appel, sans attendre : il croyait donc l'écriture terminée
2083
2145
  // alors qu'elle venait de PARTIR. Sa garantie « une seule en vol, la dernière gagne » ne
2084
2146
  // s'appliquait qu'à l'ordre des APPELS, pas à celui des écritures — plusieurs PATCH pouvaient
2085
2147
  // voler ensemble, et une position ancienne atterrir après une récente.
2086
2148
  function presentContent(content){ if(!PRES)return Promise.resolve(); var h={'Content-Type':'application/json'}; var tk=appToken(); if(tk) h['Authorization']='Bearer '+tk;
2087
- return fetch('/api/doc',{method:'POST',headers:h,body:JSON.stringify({action:'present-content',slug:PRES.slug,content:content})})
2149
+ return fetch('/api/doc',{method:'POST',headers:h,body:JSON.stringify({action:'present-content',slug:PRES.slug,control:PRES.control,content:content})})
2088
2150
  .then(function(r){ if(r&&r.ok)diffuserEtat(); })
2089
2151
  .catch(function(){}); }
2090
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); }
@@ -2798,8 +2860,18 @@ async function handler(req, res) {
2798
2860
  // ⚠️ Une identité prouvée REMPLACE celle qu'on affirme — elle ne s'y ajoute pas. Sinon la
2799
2861
  // vérification ne servirait qu'à décorer une affirmation qu'on croit toujours.
2800
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.
2801
2873
  const r = await recordAttendance(String(body.slug || ""), {
2802
- key: body.key,
2874
+ key: profil ? profil.email : cleAnonyme(body.key),
2803
2875
  name: (profil && profil.name) || body.name,
2804
2876
  email: profil ? profil.email : body.email,
2805
2877
  avatar: (profil && profil.avatar) || body.avatar,
@@ -2813,8 +2885,18 @@ async function handler(req, res) {
2813
2885
  const jp = (status, obj) => { res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(obj)); };
2814
2886
  try {
2815
2887
  const u = await PLAYER.identity.verifyToken(req.headers.authorization);
2816
- if (!u || !u.email) return jp(401, { ok: false, error: "auth" });
2817
- 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);
2818
2900
  let r;
2819
2901
  if (body.action === "present-list") r = { ok: true, presentations: await listActivePresentations(u.email) };
2820
2902
  else if (body.action === "present-reclaim") r = await reclaimPresentation(String(body.slug || ""), u.email);
@@ -2825,7 +2907,7 @@ async function handler(req, res) {
2825
2907
  if (!isAllowedStorageUrl(String(body.fileUrl || ""))) return jp(400, { ok: false, error: "url" });
2826
2908
  r = await switchPresentationDoc(String(body.slug || ""), u.email, isAdmin, { fileUrl: body.fileUrl, fileName: body.fileName, docTitle: body.docTitle, docId: body.docId });
2827
2909
  }
2828
- 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 || ""));
2829
2911
  else r = await handoverPresentation(String(body.slug || ""), u.email, body.newOwner);
2830
2912
  return jp(r.ok ? 200 : (r.status || 400), r);
2831
2913
  } catch { return jp(500, { ok: false }); }
@@ -3295,6 +3377,36 @@ async function handler(req, res) {
3295
3377
  // Mode « Présenter » côté AUDIENCE : `?present=<slug>` → page live (suit le présentateur via Realtime) ;
3296
3378
  // `&file=1` → stream le PDF de la présentation (Range, même origine pour pdf.js).
3297
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
+ }
3298
3410
  const pres = await getPresentation(String(q.present));
3299
3411
  if (!pres) return sendRefusal(res, "ended", embed);
3300
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,