discovery-media-player 0.1.45 → 0.1.46
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/docs/HOST-CONTRACT.md +29 -1
- package/package.json +1 -1
- package/server/browser.generated.js +1 -1
- package/server/cache.js +82 -0
- package/server/handler.js +139 -46
- package/server/presentations.js +47 -4
- package/server/shared.generated.js +10 -1
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -53,10 +53,38 @@ therefore write **no wiring code at all**, only environment variables.
|
|
|
53
53
|
### 1. Who may manage tracked links
|
|
54
54
|
|
|
55
55
|
```
|
|
56
|
-
POST → { "email": "…", "role": "…", "action": "
|
|
56
|
+
POST → { "email": "…", "role": "…", "action": "<one of the names below>" }
|
|
57
57
|
← { "allowed": true }
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
| action | what it grants |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `create` | create a tracked link |
|
|
63
|
+
| `list` | list one's own links |
|
|
64
|
+
| `list.all` | list everyone's links |
|
|
65
|
+
| `revoke` | revoke a link |
|
|
66
|
+
| `setauth` | change a link's access wall |
|
|
67
|
+
| `overview` | read a document's aggregate figures |
|
|
68
|
+
| `sessions` | read individual reading sessions |
|
|
69
|
+
| `test` | create a rehearsal link |
|
|
70
|
+
| `presentations.list.all` | list presentations **one does not own** (slugs, presenter names, counts) |
|
|
71
|
+
| `presentations.stats` | read the **attendees** of a presentation one does not own — names, addresses, dwell time, pages |
|
|
72
|
+
|
|
73
|
+
⚠️ **This list grows, and a closed table refuses what it has not heard of.** Both
|
|
74
|
+
`presentations.*` names arrived in `0.1.46`; a host whose table predates them answers *no* to
|
|
75
|
+
everyone, administrators included — not because the right is missing, but because the table is older
|
|
76
|
+
than the player. That failure reads exactly like a permission problem, which is what makes it
|
|
77
|
+
expensive. **Compare this table against your own at each upgrade**, and prefer a refusal that names
|
|
78
|
+
the unknown action over one that looks like a role issue.
|
|
79
|
+
|
|
80
|
+
⚠️ **`presentations.list.all` and `presentations.stats` are deliberately separate.** Seeing *that* a
|
|
81
|
+
presentation happened and seeing *who attended it* are different sensitivities: the first returns
|
|
82
|
+
metadata, the second returns people — often prospects. Merging them would take from you the choice of
|
|
83
|
+
opening one to the whole team while reserving the other.
|
|
84
|
+
|
|
85
|
+
Neither is needed to read one's **own** presentations: an owner, and an administrator, are always
|
|
86
|
+
served without the player asking you anything.
|
|
87
|
+
|
|
60
88
|
- **The player verifies the token; you decide the rights.** No answer, or a failing rule, means
|
|
61
89
|
refusal. A right that cannot be granted is not granted.
|
|
62
90
|
- **`email` is the authoritative identity.** `role` is what the session token carried; a host whose
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
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 C=Object.defineProperty,$e=Object.defineProperties,He=Object.getOwnPropertyDescriptor,Ue=Object.getOwnPropertyDescriptors,De=Object.getOwnPropertyNames,he=Object.getOwnPropertySymbols;var ve=Object.prototype.hasOwnProperty,We=Object.prototype.propertyIsEnumerable;var ye=(e,n,t)=>n in e?C(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t,B=(e,n)=>{for(var t in n||(n={}))ve.call(n,t)&&ye(e,t,n[t]);if(he)for(var t of he(n))We.call(n,t)&&ye(e,t,n[t]);return e},Me=(e,n)=>$e(e,Ue(n));var v=(e,n)=>{for(var t in n)C(e,t,{get:n[t],enumerable:!0})},Ve=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of De(n))!ve.call(e,o)&&o!==t&&C(e,o,{get:()=>n[o],enumerable:!(r=He(n,o))||r.enumerable});return e};var Fe=e=>Ve(C({},\"__esModule\",{value:!0}),e);var Un={};v(Un,{bridge:()=>L,cadence:()=>N,chat:()=>H,live:()=>$,presentation:()=>U,presentationState:()=>W,tracking:()=>O,viewer:()=>D});var L={};v(L,{onHostMessage:()=>Qe,onPlayerMessage:()=>Xe,parseHostMessage:()=>Te,parsePlayerMessage:()=>_e,sendToHost:()=>Ye,sendToPlayer:()=>Ze,toWire:()=>Y});var K=\"3dd-doc-\",ze=/^[A-Za-z0-9_-]{1,64}$/,je=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Ge=[\"embed-denied\"],Be=[\"present-invite\",\"present-handover\",\"present-switch\"],Ke=[\"handover-done\"];function Y(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 xe(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(Ge.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\"||!ze.test(s)?null:{type:o,slug:s}}function _e(e){return xe(e,je,Be)}function Te(e){return xe(e,Ke,[])}function Ye(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(Y(e),\"*\")}catch(t){}}function Ze(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(Y(n),\"*\")}catch(r){}}function qe(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function Xe(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=s=>{if(!qe(t,s.source))return;let a=_e(s.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function Qe(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 l=Te(a.data);l&&e(l)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var O={};v(O,{createTracker:()=>sn});var N={};v(N,{PRESENTER_ACTIONS_PER_HOUR:()=>Q,PRESENT_QUOTA_PER_HOUR:()=>nn,PRESENT_READS_PER_HOUR:()=>ke,PRESENT_READ_BURST:()=>Re,PRESENT_RESYNC_MS:()=>Ee,PRESENT_SIGNAL_BUDGET_PER_HOUR:()=>en,PRESENT_WRITE_TIMEOUT_MS:()=>J,READERS_PER_EGRESS:()=>q,RESYNC_READS_PER_HOUR:()=>Se,SESSION_IDLE_MS:()=>X,SESSION_INTERVAL_MS:()=>Z,SESSION_QUOTA_PER_HOUR:()=>Je,SESSION_WRITES_PER_HOUR:()=>we});var Z=12e3,we=Math.ceil(300),q=25,Je=we*q,X=18e4,Ee=25e3,Se=Math.ceil(36e5/Ee),Q=720,ke=Se+Q,en=Q,Re=20,nn=(ke+Re)*q,J=1e4;var tn=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function rn(){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 on(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 sn(e={}){var me,ge;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=(me=e.idleMs)!=null?me:X,a=(ge=e.sessionEveryMs)!=null?ge:12e3,l=e.slug||\"\",p=e.internal||null,m=e.sessionId||rn(),b=e.send||on(o,n),f={},u=0,h=0,c=0,M=0,y=null,w=r(),E=!1,V=!1,P=[],Oe=()=>!!l||!!p,ue=i=>{if(Oe())try{b(i)}catch(d){}},F=()=>!!t&&t.visibilityState===\"visible\"&&!E,ce=i=>{let d=r()-i,x=Math.max(0,w-i)+s;return Math.max(0,Math.min(d,x))},z=()=>{y!=null&&u>0&&(f[u]=(f[u]||0)+ce(y)/1e3),y=F()?r():null},de=()=>{z(),y=null},j=()=>{y==null&&F()&&(y=r())},G=()=>{let i=0;for(let d in f)i+=f[d];return y!=null&&(i+=ce(y)/1e3),Math.round(i)},pe=()=>{let i={};for(let d in f)i[d]=Math.round(f[d]);return i},S=()=>{w=r(),E&&(E=!1,j())},fe=(i,d)=>{p||ue({slug:l,event:i,page:d||h,maxPage:c,seconds:G(),sessionId:m})},A=()=>{z();let i={event:\"session\",sessionId:m,numPages:M,maxPage:c,totalSeconds:G(),pagesTime:pe()};p?(i.internal=!0,i.docId=p.docId,i.email=p.email,i.name=p.name,p.it&&(i.it=p.it)):i.slug=l,ue(i)},k=(i,d,x,be)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(d,x,be),P.push(()=>{i.removeEventListener(d,x,be)}))};return{sessionId:m,totalSeconds:G,maxPage:()=>c,pageTimes:pe,start(){if(V)return;V=!0,fe(\"open\",1),u=1,w=r(),y=F()?r():null,k(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(de(),A()):(S(),j())}),k(n,\"focus\",()=>{S(),j()});for(let x of tn)k(n,x,S,{passive:!0});k(e.scrollElement,\"scroll\",S,{passive:!0}),k(n,\"beforeunload\",A);let i=n.setInterval(()=>{!E&&r()-w>s&&(E=!0,de())},5e3),d=n.setInterval(A,a);P.push(()=>{n.clearInterval(i),n.clearInterval(d)})},setPage(i){!i||i===h||(w=r(),z(),u=i,h=i,i>c&&(c=i,fe(\"page\",i)))},setPageCount(i){M=i||0},noteActivity:S,flush:A,stop(){for(;P.length;){let i=P.pop();try{i==null||i()}catch(d){}}V=!1}}}var $={};v($,{STORAGE_KEYS:()=>R,attendeeKey:()=>cn,authorToken:()=>dn,avatarHtml:()=>ee,canModerate:()=>re,createBudget:()=>hn,createScheduler:()=>gn,escapeHtml:()=>g,fetchBorne:()=>bn,flattenPresence:()=>an,formatMessageBody:()=>ne,initials:()=>Ie,isMentioned:()=>pn,isMine:()=>_,reactorId:()=>te,shouldNotify:()=>fn,unreadLabel:()=>mn});var R={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\"};function g(e){return String(e==null?\"\":e).replace(/&/g,\"&\").replace(/</g,\"<\").replace(/>/g,\">\").replace(/\"/g,\""\")}function Ie(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function ee(e,n){return e?`<img src=\"${g(e)}\" alt=\"\">`:g(Ie(n))}function ne(e){let n=g(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 an(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 ln(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function un(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function cn(e,n=\"anon\",t=Pe){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 Pe(){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 dn(e,n=Pe){let t=ln(e,R.authorToken);if(t)return t;let r=n();return un(e,R.authorToken,r),r}function te(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 re(e){return!!(e&&e.role===\"presenter\")}function pn(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 fn(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||_(n,t)?!1:o}function mn(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function gn(e,n={}){var h;let t=(h=n.minMs)!=null?h:300,r=n.now||(()=>Date.now()),o=n.setTimer||((c,M)=>setTimeout(c,M)),s=n.clearTimer||(c=>clearTimeout(c)),a=!1,l=!1,p=-1/0,m=null,b=!1;function f(){if(b||a){l=!0;return}let c=t-(r()-p);if(c>0){u(c);return}a=!0,l=!1,p=r();let M=!1;e(()=>{M||(M=!0,a=!1,l&&!b&&f())})}function u(c){b||m!==null||(m=o(()=>{m=null,f()},Math.max(0,c)))}return{signaler(){b||f()},maintenant(){b||(p=-1/0,f())},arreter(){b=!0,m!==null&&(s(m),m=null)}}}function bn(e,n,t=J,r={}){let o=r.fetch||(typeof fetch==\"function\"?fetch:void 0);if(!o)return Promise.reject(new Error(\"fetch indisponible\"));let s=r.setTimer||((u,h)=>setTimeout(u,h)),a=r.clearTimer||(u=>clearTimeout(u)),l=null;try{l=new AbortController}catch(u){}let p=l?Me(B({},n||{}),{signal:l.signal}):B({},n||{}),m=null,b=new Promise((u,h)=>{m=s(()=>{try{l==null||l.abort()}catch(c){}h(new Error(\"delai\"))},t)}),f=()=>{try{a(m)}catch(u){}};return Promise.race([o(e,p),b]).then(u=>(f(),u),u=>{throw f(),u})}function hn(e){let n=e.now||(()=>Date.now()),t=Math.max(1,e.rafale),r=t,o=n();return{prendre(){let s=n();return r=Math.min(t,r+(s-o)*e.parHeure/36e5),o=s,r<1?!1:(r-=1,!0)},restants:()=>r}}var H={};v(H,{messageClassName:()=>vn,renderActions:()=>Le,renderAttachment:()=>Ce,renderMessage:()=>yn,renderReactions:()=>Ae});function Ae(e,n){let t=e&&e.reactions||{},r=te(n),o=\"\";for(let s in t){let a=t[s]||[];if(!a.length)continue;let l=a.indexOf(r)>=0;o+=`<button class=\"re-chip${l?\" on\":\"\"}\" data-e=\"${g(s)}\">${g(s)} ${a.length}</button>`}return o}function Ce(e){if(!e||!e.url)return\"\";let n=g(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=g(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 Le(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||re(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function yn(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>${g(e.reply_name||\"\")}</b> ${g(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${ne(e.body)}${r}</div>`:\"\",s=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${ee(e.author_avatar,e.author_name)}</span><span class=b><div class=who><b>${g(e.author_name||\"Invit\\xE9\")}</b>${s}</div>`+t+o+Ce(e.attachment)+`<div class=cm-re>${Ae(e,n.me)}</div></span><span class=cm-act>${Le(e,n)}</span>`}function vn(e,n,t){return\"cm\"+(_(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var U={};v(U,{DEFAULT_CENTER:()=>se,DEFAULT_ZOOM:()=>ae,MAP_TYPES:()=>I,cycleMapType:()=>Mn,initialMapContent:()=>_n,mapTypeLabel:()=>xn,sanitizeContent:()=>le});var I=[\"roadmap\",\"satellite\",\"hybrid\"],se=[46.6,2.5],ae=6;function T(e){let n=Number(e);return Number.isFinite(n)?n:null}function oe(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 ie=(e,n,t)=>Math.max(n,Math.min(t,e));function le(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=oe(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:ie(T(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:ie(T(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:oe(n.center)||[...se],zoom:ie(Math.trunc(T(n.zoom)||ae),1,21),marker:oe(n.marker),mapType:I.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function Mn(e){let n=I.indexOf(e);return I[(n+1)%I.length]}function xn(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function _n(){return{kind:\"map\",center:[...se],zoom:ae,marker:null,mapType:null,label:null}}var D={};v(D,{CURRENT_PAGE_MARGIN:()=>Rn,DEFAULT_ASPECT:()=>En,MAX_ZOOM:()=>wn,MIN_PAGE_WIDTH:()=>Sn,MIN_ZOOM:()=>Tn,PRERENDER_MARGIN:()=>kn,arrowState:()=>An,averageColor:()=>Nn,clampPage:()=>Ne,clampZoom:()=>In,fitWidth:()=>Ln,isImageDocument:()=>On,progressPercent:()=>Pn});var Tn=.5,wn=3,En=1.35,Sn=280,kn=\"500px 0px\",Rn=\"-48% 0px -48% 0px\";function In(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function Ne(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function Pn(e,n){return n?Math.round(Ne(e,n)/n*100):0}function An(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var Cn=28;function Ln(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)-Cn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function Nn(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 On(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var W={};v(W,{presentationTransition:()=>$n,switchDocUrl:()=>Hn});function $n(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=le(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 Fe(Un);})();", SOURCE_HASH: "5d92c853f7d8ba79" };
|
|
7
|
+
module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var C=Object.defineProperty,Ue=Object.defineProperties,De=Object.getOwnPropertyDescriptor,We=Object.getOwnPropertyDescriptors,Ve=Object.getOwnPropertyNames,ye=Object.getOwnPropertySymbols;var Me=Object.prototype.hasOwnProperty,Fe=Object.prototype.propertyIsEnumerable;var ve=(e,n,t)=>n in e?C(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t,B=(e,n)=>{for(var t in n||(n={}))Me.call(n,t)&&ve(e,t,n[t]);if(ye)for(var t of ye(n))Fe.call(n,t)&&ve(e,t,n[t]);return e},xe=(e,n)=>Ue(e,We(n));var v=(e,n)=>{for(var t in n)C(e,t,{get:n[t],enumerable:!0})},ze=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of Ve(n))!Me.call(e,o)&&o!==t&&C(e,o,{get:()=>n[o],enumerable:!(r=De(n,o))||r.enumerable});return e};var je=e=>ze(C({},\"__esModule\",{value:!0}),e);var zn={};v(zn,{bridge:()=>L,cadence:()=>N,chat:()=>H,live:()=>$,presentation:()=>U,presentationState:()=>W,tracking:()=>O,viewer:()=>D});var L={};v(L,{onHostMessage:()=>en,onPlayerMessage:()=>Je,parseHostMessage:()=>Te,parsePlayerMessage:()=>we,sendToHost:()=>qe,sendToPlayer:()=>Qe,toWire:()=>Y});var K=\"3dd-doc-\",Ge=/^[A-Za-z0-9_-]{1,64}$/,Be=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Ke=[\"embed-denied\"],Ye=[\"present-invite\",\"present-handover\",\"present-switch\"],Ze=[\"handover-done\"];function Y(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 _e(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(Ke.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 i=e.slug;return typeof i!=\"string\"||!Ge.test(i)?null:{type:o,slug:i}}function we(e){return _e(e,Be,Ye)}function Te(e){return _e(e,Ze,[])}function qe(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(Y(e),\"*\")}catch(t){}}function Qe(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(Y(n),\"*\")}catch(r){}}function Xe(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function Je(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=i=>{if(!Xe(t,i.source))return;let a=we(i.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function en(e,n){var i;let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=(i=t.parent)!=null?i:null,o=a=>{if(!r||r===t||a.source!==r)return;let l=Te(a.data);l&&e(l)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var O={};v(O,{createTracker:()=>ln});var N={};v(N,{PRESENTER_ACTIONS_PER_HOUR:()=>X,PRESENT_CACHE_MS:()=>rn,PRESENT_QUOTA_MARGIN:()=>Ae,PRESENT_QUOTA_PER_HOUR:()=>on,PRESENT_READS_PER_HOUR:()=>Re,PRESENT_READ_BURST:()=>Ie,PRESENT_READ_COALESCE_MS:()=>Pe,PRESENT_RESYNC_MS:()=>Se,PRESENT_SIGNAL_BUDGET_PER_HOUR:()=>tn,PRESENT_WRITE_TIMEOUT_MS:()=>J,READERS_PER_EGRESS:()=>q,RESYNC_READS_PER_HOUR:()=>ke,SESSION_IDLE_MS:()=>Q,SESSION_INTERVAL_MS:()=>Z,SESSION_QUOTA_PER_HOUR:()=>nn,SESSION_WRITES_PER_HOUR:()=>Ee});var Z=12e3,Ee=Math.ceil(300),q=25,nn=Ee*q,Q=18e4,Se=25e3,ke=Math.ceil(36e5/Se),X=720,Re=ke+X,tn=X,Ie=20,J=1e4,Pe=400,rn=Pe,Ae=4,on=(Re+Ie)*q*Ae;var sn=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function an(){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 un(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 ln(e={}){var ge,be;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\",i=(ge=e.idleMs)!=null?ge:Q,a=(be=e.sessionEveryMs)!=null?be:12e3,l=e.slug||\"\",d=e.internal||null,m=e.sessionId||an(),p=e.send||un(o,n),c={},u=0,g=0,f=0,M=0,y=null,E=r(),S=!1,V=!1,P=[],He=()=>!!l||!!d,ce=s=>{if(He())try{p(s)}catch(b){}},F=()=>!!t&&t.visibilityState===\"visible\"&&!S,de=s=>{let b=r()-s,_=Math.max(0,E-s)+i;return Math.max(0,Math.min(b,_))},z=()=>{y!=null&&u>0&&(c[u]=(c[u]||0)+de(y)/1e3),y=F()?r():null},pe=()=>{z(),y=null},j=()=>{y==null&&F()&&(y=r())},G=()=>{let s=0;for(let b in c)s+=c[b];return y!=null&&(s+=de(y)/1e3),Math.round(s)},fe=()=>{let s={};for(let b in c)s[b]=Math.round(c[b]);return s},k=()=>{E=r(),S&&(S=!1,j())},me=(s,b)=>{d||ce({slug:l,event:s,page:b||g,maxPage:f,seconds:G(),sessionId:m})},A=()=>{z();let s={event:\"session\",sessionId:m,numPages:M,maxPage:f,totalSeconds:G(),pagesTime:fe()};d?(s.internal=!0,s.docId=d.docId,s.email=d.email,s.name=d.name,d.it&&(s.it=d.it)):s.slug=l,ce(s)},R=(s,b,_,he)=>{!s||typeof s.addEventListener!=\"function\"||(s.addEventListener(b,_,he),P.push(()=>{s.removeEventListener(b,_,he)}))};return{sessionId:m,totalSeconds:G,maxPage:()=>f,pageTimes:fe,start(){if(V)return;V=!0,me(\"open\",1),u=1,E=r(),y=F()?r():null,R(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(pe(),A()):(k(),j())}),R(n,\"focus\",()=>{k(),j()});for(let _ of sn)R(n,_,k,{passive:!0});R(e.scrollElement,\"scroll\",k,{passive:!0}),R(n,\"beforeunload\",A);let s=n.setInterval(()=>{!S&&r()-E>i&&(S=!0,pe())},5e3),b=n.setInterval(A,a);P.push(()=>{n.clearInterval(s),n.clearInterval(b)})},setPage(s){!s||s===g||(E=r(),z(),u=s,g=s,s>f&&(f=s,me(\"page\",s)))},setPageCount(s){M=s||0},noteActivity:k,flush:A,stop(){for(;P.length;){let s=P.pop();try{s==null||s()}catch(b){}}V=!1}}}var $={};v($,{STORAGE_KEYS:()=>x,attendeeKey:()=>fn,authorToken:()=>mn,avatarHtml:()=>ee,canModerate:()=>oe,createBudget:()=>Mn,createFileEcritures:()=>xn,createScheduler:()=>yn,escapeHtml:()=>h,fetchBorne:()=>vn,flattenPresence:()=>cn,formatMessageBody:()=>ne,initials:()=>Ce,isMentioned:()=>gn,isMine:()=>w,presenceId:()=>_n,reactorId:()=>re,shouldNotify:()=>bn,unreadLabel:()=>hn});var x={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\",presenceId:\"3dd-present-uid\"};function h(e){return String(e==null?\"\":e).replace(/&/g,\"&\").replace(/</g,\"<\").replace(/>/g,\">\").replace(/\"/g,\""\")}function Ce(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function ee(e,n){return e?`<img src=\"${h(e)}\" alt=\"\">`:h(Ce(n))}function ne(e){let n=h(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 cn(e){let n={},t=[];for(let r in e||{})for(let o of(e||{})[r]||[]){let i=String(o.uid||o.email||o.name||\"\").toLowerCase()||`_${r}_${t.length}`;n[i]?o.role===\"presenter\"&&(n[i]=o):(n[i]=o,t.push(i))}return t.map(r=>n[r])}function dn(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function pn(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function fn(e,n=\"anon\",t=te){try{if(!e)return`anon-${n}`;let r=e.getItem(x.attendeeKey);if(r)return r;let o=`anon-${t()}`;return e.setItem(x.attendeeKey,o),o}catch(r){return`anon-${n}`}}function te(){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 mn(e,n=te){let t=dn(e,x.authorToken);if(t)return t;let r=n();return pn(e,x.authorToken,r),r}function re(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 oe(e){return!!(e&&e.role===\"presenter\")}function gn(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 bn(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||w(n,t)?!1:o}function hn(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function yn(e,n={}){var g;let t=(g=n.minMs)!=null?g:300,r=n.now||(()=>Date.now()),o=n.setTimer||((f,M)=>setTimeout(f,M)),i=n.clearTimer||(f=>clearTimeout(f)),a=!1,l=!1,d=-1/0,m=null,p=!1;function c(){if(p||a){l=!0;return}let f=t-(r()-d);if(f>0){u(f);return}a=!0,l=!1,d=r();let M=!1;e(()=>{M||(M=!0,a=!1,l&&!p&&c())})}function u(f){p||m!==null||(m=o(()=>{m=null,c()},Math.max(0,f)))}return{signaler(){p||c()},maintenant(){p||(d=-1/0,c())},arreter(){p=!0,m!==null&&(i(m),m=null)}}}function vn(e,n,t=J,r={}){let o=r.fetch||(typeof fetch==\"function\"?fetch:void 0);if(!o)return Promise.reject(new Error(\"fetch indisponible\"));let i=r.setTimer||((u,g)=>setTimeout(u,g)),a=r.clearTimer||(u=>clearTimeout(u)),l=null;try{l=new AbortController}catch(u){}let d=l?xe(B({},n||{}),{signal:l.signal}):B({},n||{}),m=null,p=new Promise((u,g)=>{m=i(()=>{try{l==null||l.abort()}catch(f){}g(new Error(\"delai\"))},t)}),c=()=>{try{a(m)}catch(u){}};return Promise.race([o(e,d),p]).then(u=>(c(),u),u=>{throw c(),u})}function Mn(e){let n=e.now||(()=>Date.now()),t=Math.max(1,e.rafale),r=t,o=n();return{prendre(){let i=n();return r=Math.min(t,r+(i-o)*e.parHeure/36e5),o=i,r<1?!1:(r-=1,!0)},restants:()=>r}}function xn(e={}){var m;let n=(m=e.minMs)!=null?m:0,t=e.now||(()=>Date.now()),r=e.setTimer||((p,c)=>setTimeout(p,c)),o=new Map,i=!1,a=-1/0,l=!1;function d(){if(i||o.size===0)return;let p=n-(t()-a);if(p>0){if(l)return;l=!0,r(()=>{l=!1,d()},p);return}let c=o.keys().next().value,u=o.get(c);o.delete(c),i=!0,a=t(),Promise.resolve().then(u.tache).then(g=>u.regler(g),()=>u.regler(null)).then(()=>{i=!1,d()})}return{poser(p,c){return new Promise(u=>{let g=o.get(p);o.set(p,{tache:c,regler:f=>{g&&g.regler(f),u(f)}}),d()})},enAttente:()=>o.size}}function _n(e,n=te){try{if(e){let t=e.getItem(x.presenceId);if(t)return t;let r=`p-${n()}`;return e.setItem(x.presenceId,r),r}}catch(t){}return`p-${n()}`}var H={};v(H,{messageClassName:()=>Tn,renderActions:()=>Oe,renderAttachment:()=>Ne,renderMessage:()=>wn,renderReactions:()=>Le});function Le(e,n){let t=e&&e.reactions||{},r=re(n),o=\"\";for(let i in t){let a=t[i]||[];if(!a.length)continue;let l=a.indexOf(r)>=0;o+=`<button class=\"re-chip${l?\" on\":\"\"}\" data-e=\"${h(i)}\">${h(i)} ${a.length}</button>`}return o}function Ne(e){if(!e||!e.url)return\"\";let n=h(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=h(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 Oe(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||oe(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function wn(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>${h(e.reply_name||\"\")}</b> ${h(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${ne(e.body)}${r}</div>`:\"\",i=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${ee(e.author_avatar,e.author_name)}</span><span class=b><div class=who><b>${h(e.author_name||\"Invit\\xE9\")}</b>${i}</div>`+t+o+Ne(e.attachment)+`<div class=cm-re>${Le(e,n.me)}</div></span><span class=cm-act>${Oe(e,n)}</span>`}function Tn(e,n,t){return\"cm\"+(w(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var U={};v(U,{DEFAULT_CENTER:()=>ae,DEFAULT_ZOOM:()=>ue,MAP_TYPES:()=>I,cycleMapType:()=>En,initialMapContent:()=>kn,mapTypeLabel:()=>Sn,sanitizeContent:()=>le});var I=[\"roadmap\",\"satellite\",\"hybrid\"],ae=[46.6,2.5],ue=6;function T(e){let n=Number(e);return Number.isFinite(n)?n:null}function ie(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 se=(e,n,t)=>Math.max(n,Math.min(t,e));function le(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=ie(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:se(T(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:se(T(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:ie(n.center)||[...ae],zoom:se(Math.trunc(T(n.zoom)||ue),1,21),marker:ie(n.marker),mapType:I.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function En(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 kn(){return{kind:\"map\",center:[...ae],zoom:ue,marker:null,mapType:null,label:null}}var D={};v(D,{CURRENT_PAGE_MARGIN:()=>Ln,DEFAULT_ASPECT:()=>Pn,MAX_ZOOM:()=>In,MIN_PAGE_WIDTH:()=>An,MIN_ZOOM:()=>Rn,PRERENDER_MARGIN:()=>Cn,arrowState:()=>$n,averageColor:()=>Dn,clampPage:()=>$e,clampZoom:()=>Nn,fitWidth:()=>Un,isImageDocument:()=>Wn,progressPercent:()=>On});var Rn=.5,In=3,Pn=1.35,An=280,Cn=\"500px 0px\",Ln=\"-48% 0px -48% 0px\";function Nn(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function $e(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function On(e,n){return n?Math.round($e(e,n)/n*100):0}function $n(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var Hn=28;function Un(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,i=((e.containerHeight||600)-Hn-(e.overlap||0)-(e.reserve||0))/r;return i>120?Math.min(t,i):t}function Dn(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let i=0;i<e.length;i+=4)t+=e[i],r+=e[i+1],o+=e[i+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function Wn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var W={};v(W,{presentationTransition:()=>Vn,switchDocUrl:()=>Fn});function Vn(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=le(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 Fn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return je(zn);})();", SOURCE_HASH: "0308434f00e5e31f" };
|
package/server/cache.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// UN CACHE COURT SUR LE CHEMIN DE LECTURE PUBLIC.
|
|
2
|
+
//
|
|
3
|
+
// ⚠️ IL NE REMPLACE PAS UNE LIMITE, IL SUPPRIME CE QU'ELLE GARDAIT. `state=1` et `chat=1` lisent une
|
|
4
|
+
// ligne identique pour tous les spectateurs d'une même présentation à un instant donné. Les relire
|
|
5
|
+
// une fois par spectateur n'apporte rien ; les relire une fois par FENÊTRE fait s'effondrer
|
|
6
|
+
// n'importe quelle cadence — légitime ou hostile — à un accès base, quel que soit le nombre
|
|
7
|
+
// d'appelants. Il n'y a plus de ressource à saturer, donc plus de victime.
|
|
8
|
+
//
|
|
9
|
+
// L'idée vient du second hôte, qui a vu plus loin que notre quota : « la cadence de relecture est
|
|
10
|
+
// imposée par qui diffuse, donc tout quota par appelant est une arme retournée ».
|
|
11
|
+
//
|
|
12
|
+
// ⚠️ TROIS PIÈGES, ET ILS SONT TOUS DANS CE FICHIER.
|
|
13
|
+
//
|
|
14
|
+
// 1. LA RAFALE FROIDE. Vingt-cinq spectateurs qui arrivent ensemble sur une clé absente
|
|
15
|
+
// produiraient vingt-cinq lectures si l'on ne mémorisait que le RÉSULTAT. On mémorise donc la
|
|
16
|
+
// PROMESSE : le premier déclenche, les autres attendent la même. C'est exactement le cas que le
|
|
17
|
+
// cache existe pour couvrir — le rater le viderait de son sens dans la seule situation qui
|
|
18
|
+
// compte.
|
|
19
|
+
//
|
|
20
|
+
// 2. LA CLÉ VIENT DE L'APPELANT. Le slug est dans l'URL : n'importe qui peut en demander un million
|
|
21
|
+
// de différents. Une table sans borne deviendrait une fuite mémoire commandée depuis l'extérieur.
|
|
22
|
+
// On plafonne, et on évince le plus ancien.
|
|
23
|
+
//
|
|
24
|
+
// 3. UN ÉCHEC NE SE MÉMORISE PAS. Servir une erreur pendant toute la fenêtre transformerait un
|
|
25
|
+
// hoquet en panne visible, et retarderait le rétablissement. On oublie la promesse rompue.
|
|
26
|
+
//
|
|
27
|
+
// ⚠️ CE QUE CE CACHE NE FAIT PAS : il vit dans la mémoire du PROCESSUS. En serverless, plusieurs
|
|
28
|
+
// instances servent en parallèle et démarrent à froid — l'effondrement est donc « une lecture par
|
|
29
|
+
// fenêtre ET PAR INSTANCE », pas « une lecture ». C'est la même limite que celle d'un compteur de
|
|
30
|
+
// débit en mémoire, et elle est écrite ici plutôt que découverte plus tard. Elle reste un
|
|
31
|
+
// effondrement de plusieurs ordres de grandeur.
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Un cache à durée de vie courte, avec regroupement des demandes concurrentes.
|
|
35
|
+
*
|
|
36
|
+
* @param {{ ttlMs: number, max?: number, now?: () => number }} options
|
|
37
|
+
*/
|
|
38
|
+
function creerCache(options) {
|
|
39
|
+
const ttl = Math.max(0, Number(options && options.ttlMs) || 0);
|
|
40
|
+
const max = Math.max(1, Number(options && options.max) || 500);
|
|
41
|
+
const now = (options && options.now) || (() => Date.now());
|
|
42
|
+
/** @type {Map<string, { echeance: number, promesse: Promise<unknown> }>} */
|
|
43
|
+
const entrees = new Map();
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
/**
|
|
47
|
+
* Rend la valeur en cache si elle est fraîche, sinon la produit — une seule fois pour tous les
|
|
48
|
+
* appelants concurrents.
|
|
49
|
+
*/
|
|
50
|
+
async lire(cle, produire) {
|
|
51
|
+
const k = String(cle);
|
|
52
|
+
const t = now();
|
|
53
|
+
const vue = entrees.get(k);
|
|
54
|
+
if (vue && vue.echeance > t) return vue.promesse;
|
|
55
|
+
|
|
56
|
+
const promesse = Promise.resolve().then(produire);
|
|
57
|
+
entrees.set(k, { echeance: t + ttl, promesse });
|
|
58
|
+
|
|
59
|
+
// ⚠️ On oublie une promesse ROMPUE, jamais une promesse tenue : sinon un hoquet de base
|
|
60
|
+
// resterait servi pendant toute la fenêtre, et le rétablissement attendrait pour rien.
|
|
61
|
+
promesse.catch(() => {
|
|
62
|
+
const courante = entrees.get(k);
|
|
63
|
+
if (courante && courante.promesse === promesse) entrees.delete(k);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Éviction : la plus ancienne d'abord (Map conserve l'ordre d'insertion). La clé venant de
|
|
67
|
+
// l'appelant, une table sans borne serait une fuite mémoire commandée depuis l'extérieur.
|
|
68
|
+
if (entrees.size > max) {
|
|
69
|
+
for (const vieille of entrees.keys()) {
|
|
70
|
+
entrees.delete(vieille);
|
|
71
|
+
if (entrees.size <= max) break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return promesse;
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
/** Pour les tests et l'exploitation : ce que la table contient réellement. */
|
|
78
|
+
taille: () => entrees.size,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { creerCache };
|
package/server/handler.js
CHANGED
|
@@ -6,7 +6,15 @@
|
|
|
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, PRESENT_QUOTA_PER_HOUR } = require("./shared.generated.js");
|
|
9
|
+
const { SESSION_QUOTA_PER_HOUR, PRESENT_QUOTA_PER_HOUR, PRESENT_CACHE_MS } = require("./shared.generated.js");
|
|
10
|
+
const { creerCache } = require("./cache.js");
|
|
11
|
+
|
|
12
|
+
// ⚠️ UN SEUL CACHE POUR TOUT LE PROCESSUS, ET C'EST LE POINT. Le créer par requête reviendrait à
|
|
13
|
+
// n'en avoir aucun : chaque appelant repartirait d'une table vide, et l'effondrement qu'on
|
|
14
|
+
// cherche — N spectateurs, une lecture — n'aurait jamais lieu. C'est aussi pourquoi il vit ici,
|
|
15
|
+
// au chargement du module, et non dans `init()` : un hôte qui réinitialise son contexte ne doit
|
|
16
|
+
// pas vider ce qui protège sa base.
|
|
17
|
+
const cacheLecture = creerCache({ ttlMs: PRESENT_CACHE_MS });
|
|
10
18
|
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
19
|
// CONTEXTE INJECTÉ : tout ce que le player emprunte à l'application hôte passe par ici — stockage,
|
|
12
20
|
// base, identité, limites, marque, journalisation — et rien d'autre. C'est la frontière qui permettra
|
|
@@ -283,6 +291,13 @@ var Live=(function(){
|
|
|
283
291
|
function _store(){try{return window.localStorage;}catch(e){return null;}}
|
|
284
292
|
// Clé d'assistance STABLE (analytics de présentation) : email si connu, sinon id persistant par navigateur.
|
|
285
293
|
function attKey(){ return Player.live.attendeeKey(_store(),MYID); }
|
|
294
|
+
// ⚠️ CE QUI PART DANS LA PRÉSENCE N'EST PAS CE QUI IDENTIFIE UNE LIGNE DE MESURE. La présence est
|
|
295
|
+
// diffusée à toute l'audience ; y mettre la clé de participation revenait à donner à chacun de
|
|
296
|
+
// quoi écraser la ligne de son voisin. Deux besoins, deux valeurs.
|
|
297
|
+
// Mémorisé pour la durée de la page : sans stockage, la fonction rend une valeur neuve à chaque
|
|
298
|
+
// appel — c'est ce qui garantit qu'elle ne partage aucune graine avec la clé de mesure.
|
|
299
|
+
var _presId=null;
|
|
300
|
+
function presId(){ if(!_presId)_presId=Player.live.presenceId(_store()); return _presId; }
|
|
286
301
|
// Heartbeat d'assistance → le serveur journalise qui suit, combien de temps, et les pages vues (via la page
|
|
287
302
|
// courante de la présentation). Envoyé à la connexion puis toutes les 25 s. Best-effort (silencieux).
|
|
288
303
|
// Le serveur ne croit plus 'isMember' ni 'isPresenter' sur parole : l'appartenance se prouve par
|
|
@@ -494,7 +509,10 @@ var Live=(function(){
|
|
|
494
509
|
.then(function(d){if(d&&d.ok)applique(d);})
|
|
495
510
|
.catch(function(){})
|
|
496
511
|
.then(fini,fini);
|
|
497
|
-
|
|
512
|
+
// ⚠️ La fenêtre vient de la constante partagée, pas d'un nombre écrit ici : le cache serveur
|
|
513
|
+
// se déduit d'elle, et deux nombres séparés finiraient par diverger — un cache plus long que
|
|
514
|
+
// le regroupement ajouterait une attente que le spectateur n'a pas consentie.
|
|
515
|
+
},{minMs:(window.Player&&Player.cadence&&Player.cadence.PRESENT_READ_COALESCE_MS)||400});
|
|
498
516
|
var budget=Player.live.createBudget({
|
|
499
517
|
parHeure:Player.cadence.PRESENT_SIGNAL_BUDGET_PER_HOUR,
|
|
500
518
|
rafale:Player.cadence.PRESENT_READ_BURST,
|
|
@@ -555,7 +573,7 @@ var Live=(function(){
|
|
|
555
573
|
// par accident : le jour où quelqu'un rebranche un paramètre, la charge d'un canal public
|
|
556
574
|
// redeviendrait crue sans que rien ne le signale. On coupe le chemin, pas seulement l'usage.
|
|
557
575
|
ch.on('broadcast',{event:'map'},function(){if(_onMap)_onMap();});
|
|
558
|
-
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:
|
|
576
|
+
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:presId()});sendAttend();}});
|
|
559
577
|
_tyIv=setInterval(renderTyping,1500);
|
|
560
578
|
_atIv=setInterval(sendAttend,25000);
|
|
561
579
|
// Filet de sécurité : au déchargement de la page/iframe (fermeture, reload, switch), on retire la présence
|
|
@@ -649,16 +667,18 @@ var Map3DD=(function(){
|
|
|
649
667
|
//
|
|
650
668
|
// Le suivi devient PAR PALIERS au lieu d'être continu (environ deux fois par seconde). C'est le
|
|
651
669
|
// prix pour que personne d'autre que le présentateur ne pilote l'écran de l'audience.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
670
|
+
// ⚠️ CET ORDONNANCEUR N'EN EST PLUS UN, ET C'EST LE CORRECTIF. Il en existait DEUX ici — un pour la
|
|
671
|
+
// carte, un pour Street View — pendant que quatre autres chemins écrivaient sans passer par eux.
|
|
672
|
+
// Deux mécaniques pour un même rôle, et une couverture d'un chemin sur trois.
|
|
673
|
+
//
|
|
674
|
+
// L'ordonnancement vit désormais dans « presentContent » elle-même : ce qu'on appelle ici finit dans
|
|
675
|
+
// la file unique, avec son regroupement par genre, son écriture unique en vol et son rythme
|
|
676
|
+
// minimum. Il ne reste de cette fonction qu'un adaptateur, pour ne pas réécrire ses appelants.
|
|
677
|
+
//
|
|
678
|
+
// Ce qu'elle garantissait — lire l'état le plus frais — reste vrai autrement : la file conserve la
|
|
679
|
+
// DERNIÈRE demande de ce genre, donc l'état du dernier mouvement.
|
|
680
|
+
function persistOrd(){
|
|
681
|
+
return { signaler: function(){ try{ if(map&&persist)persist(state()); }catch(e){} } }; }
|
|
662
682
|
function broadcast(){ if(!map)return; var n=Date.now(); if(n-_bcT<200)return; _bcT=n;
|
|
663
683
|
var o=persistOrd(); if(o)o.signaler();
|
|
664
684
|
try{ if(window.Live) Live.sendMap(); }catch(e){} }
|
|
@@ -702,13 +722,9 @@ var Map3DD=(function(){
|
|
|
702
722
|
loadGoogle(function(){ try{ new google.maps.StreetViewService().getPanorama({location:{lat:center[0],lng:center[1]},radius:80},function(data,status){ if(status==='OK'&&data&&data.location){ var ll=data.location.latLng; var content={kind:'streetview',position:[ll.lat(),ll.lng()],pov:{heading:0,pitch:0},zoom:1}; if(persist)persist(content); enterSV(content,true,persist); } else { tempHint('Pas de Street View à cet endroit.'); } }); }catch(e){ tempHint('Street View indisponible.'); } }); }
|
|
703
723
|
function svState(){ if(!pano)return null; var p=pano.getPosition(),v=pano.getPov(); if(!p)return null; return {kind:'streetview',position:[p.lat(),p.lng()],pov:{heading:v.heading,pitch:v.pitch},zoom:pano.getZoom()}; }
|
|
704
724
|
// Même règle qu'au-dessus : on persiste, on signale, on ne transporte pas la position.
|
|
705
|
-
|
|
706
|
-
function svOrd(){
|
|
707
|
-
|
|
708
|
-
var p=null; try{ if(persist&&pano)p=persist(svState()); }catch(e){}
|
|
709
|
-
if(p&&p.then)p.then(fini,fini); else fini();
|
|
710
|
-
},{minMs:500}); }
|
|
711
|
-
return _ordSv; }
|
|
725
|
+
// Même chose pour Street View : la file s'en charge, il ne reste qu'un adaptateur.
|
|
726
|
+
function svOrd(){
|
|
727
|
+
return { signaler: function(){ try{ if(persist&&pano)persist(svState()); }catch(e){} } }; }
|
|
712
728
|
function svBcast(){ var n=Date.now(); if(n-_svBcT<200)return; _svBcT=n; if(!svState())return;
|
|
713
729
|
var o=svOrd(); if(o)o.signaler();
|
|
714
730
|
try{ if(window.Live)Live.sendMap(); }catch(e){} }
|
|
@@ -2052,7 +2068,7 @@ ${LEGAL_CSS}
|
|
|
2052
2068
|
// Présentation live — GÉRÉE DANS L'IFRAME (toujours servie fraîche, en-tête no-store) → robuste même si le
|
|
2053
2069
|
// bundle React de l'app est en cache. Bandeau fixe persistant en bas : lien + copier + inviter + terminer.
|
|
2054
2070
|
// Seul l'envoi par chat (Inviter) est délégué à l'app (postMessage) car il exige le JWT.
|
|
2055
|
-
var PRES=null,
|
|
2071
|
+
var PRES=null, _hbIv=0;
|
|
2056
2072
|
// JWT de la session app (MÊME ORIGINE, localStorage) → autorise le rattachement de la présentation au membre
|
|
2057
2073
|
// (reprise / liste / transfert) et les actions authentifiées (reclaim).
|
|
2058
2074
|
function appToken(){ try{ var raw=localStorage.getItem(LIVECFG.hostAuthKey||''); if(!raw)return''; var s=JSON.parse(raw); var t=(s&&(s.access_token||(s.currentSession&&s.currentSession.access_token)||(s.session&&s.session.access_token)))||''; return t; }catch(e){ return ''; } }
|
|
@@ -2075,11 +2091,36 @@ ${LEGAL_CSS}
|
|
|
2075
2091
|
// rien, alors que l'émettre trop tôt fait relire pour rien ET perdre le changement.
|
|
2076
2092
|
// (audit P0-3)
|
|
2077
2093
|
function diffuserEtat(){ if(!PRES||!window.Live)return; try{ Live.sendState(); }catch(e){} }
|
|
2078
|
-
|
|
2079
|
-
|
|
2094
|
+
// ⚠️ UNE SEULE FILE, ET CE SONT LES FONCTIONS D'ÉCRITURE QUI L'EMPRUNTENT — PAS LEURS APPELANTS.
|
|
2095
|
+
//
|
|
2096
|
+
// Six chemins écrivaient : « pushPage », « showMap », « hideMap », « toMap », la recherche Street
|
|
2097
|
+
// View, et les deux ordonnanceurs de carte. Deux étaient protégés. Le correctif de 0.1.41, qui
|
|
2098
|
+
// annonçait des écritures de carte séquentielles, n'en couvrait qu'un chemin sur trois.
|
|
2099
|
+
//
|
|
2100
|
+
// Demander à chaque appelant de passer par la bonne porte serait une LISTE : le prochain
|
|
2101
|
+
// l'oubliera, et rien ne le dira. On range donc la file DANS « pushPage » et « presentContent » :
|
|
2102
|
+
// il n'existe plus de chemin direct, donc plus rien à oublier.
|
|
2103
|
+
//
|
|
2104
|
+
// Le rythme minimum remplace les deux ordonnanceurs qu'elle absorbe — un déplacement de carte à
|
|
2105
|
+
// la souris produirait sinon une écriture par image.
|
|
2106
|
+
var _file=null;
|
|
2107
|
+
function fileEcritures(){
|
|
2108
|
+
if(!_file&&window.Player&&Player.live&&Player.live.createFileEcritures){
|
|
2109
|
+
_file=Player.live.createFileEcritures({minMs:500});
|
|
2110
|
+
}
|
|
2111
|
+
return _file;
|
|
2112
|
+
}
|
|
2113
|
+
// ⚠️ L'ANTI-REBOND DE 250 ms A DISPARU, ET C'EST VOULU. Il repoussait l'échéance à chaque appel —
|
|
2114
|
+
// un défilement continu pouvait donc ne JAMAIS écrire. La file regroupe par genre sans jamais
|
|
2115
|
+
// repousser : la dernière page demandée part au prochain tour, et elle part.
|
|
2116
|
+
function pushPage(){ if(!PRES)return;
|
|
2117
|
+
var f=fileEcritures(); var page=cur||1;
|
|
2118
|
+
if(!f) return envoyerPage(page);
|
|
2119
|
+
return f.poser('page',function(){ return envoyerPage(cur||page); }); }
|
|
2120
|
+
function envoyerPage(page){ if(!PRES)return Promise.resolve();
|
|
2121
|
+
return Player.live.fetchBorne('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'present-page',slug:PRES.slug,control:PRES.control,page:page})})
|
|
2080
2122
|
.then(function(r){ if(r&&r.ok)diffuserEtat(); })
|
|
2081
|
-
.catch(function(){});
|
|
2082
|
-
},250); }
|
|
2123
|
+
.catch(function(){}); }
|
|
2083
2124
|
// ⚠️ TROIS GESTES, ET L'ORDRE ÉTAIT LE PIRE DES SIX. On signalait la fin, puis on COUPAIT LE
|
|
2084
2125
|
// CANAL, puis on envoyait l'avis de fin. Le signal partait donc avant l'écriture (relecture sur
|
|
2085
2126
|
// un état périmé), et la coupure avant l'envoi. L'audience pouvait ne jamais apprendre que la
|
|
@@ -2172,7 +2213,10 @@ ${LEGAL_CSS}
|
|
|
2172
2213
|
// alors qu'elle venait de PARTIR. Sa garantie « une seule en vol, la dernière gagne » ne
|
|
2173
2214
|
// s'appliquait qu'à l'ordre des APPELS, pas à celui des écritures — plusieurs PATCH pouvaient
|
|
2174
2215
|
// voler ensemble, et une position ancienne atterrir après une récente.
|
|
2175
|
-
function presentContent(content){ if(!PRES)return Promise.resolve();
|
|
2216
|
+
function presentContent(content){ if(!PRES)return Promise.resolve();
|
|
2217
|
+
var f=fileEcritures(); if(!f) return envoyerContenu(content);
|
|
2218
|
+
return f.poser('content',function(){ return envoyerContenu(content); }); }
|
|
2219
|
+
function envoyerContenu(content){ if(!PRES)return Promise.resolve(); var h={'Content-Type':'application/json'}; var tk=appToken(); if(tk) h['Authorization']='Bearer '+tk;
|
|
2176
2220
|
return Player.live.fetchBorne('/api/doc',{method:'POST',headers:h,body:JSON.stringify({action:'present-content',slug:PRES.slug,control:PRES.control,content:content})})
|
|
2177
2221
|
.then(function(r){ if(r&&r.ok)diffuserEtat(); })
|
|
2178
2222
|
.catch(function(){}); }
|
|
@@ -2938,8 +2982,29 @@ async function handler(req, res) {
|
|
|
2938
2982
|
if (body.action === "present-list") r = { ok: true, presentations: await listActivePresentations(u.email) };
|
|
2939
2983
|
else if (body.action === "present-reclaim") r = await reclaimPresentation(String(body.slug || ""), u.email);
|
|
2940
2984
|
else if (body.action === "present-owner-end") r = await endPresentationByOwner(String(body.slug || ""), u.email, isAdmin);
|
|
2941
|
-
|
|
2942
|
-
|
|
2985
|
+
// ⚠️ DEUX DROITS, PAS UN — ET LEUR NOM EST UN ÉLÉMENT DE CONTRAT.
|
|
2986
|
+
//
|
|
2987
|
+
// Voir qu'une présentation a eu lieu et voir QUI y était sont deux sensibilités
|
|
2988
|
+
// différentes : l'historique rend des slugs, des noms de présentateurs et des compteurs ;
|
|
2989
|
+
// les statistiques rendent les NOMS, les ADRESSES et le temps de présence de participants
|
|
2990
|
+
// qui sont souvent des prospects. Un hôte peut vouloir ouvrir le premier à toute l'équipe
|
|
2991
|
+
// et réserver le second — les fondre en un seul droit lui retirerait ce choix.
|
|
2992
|
+
//
|
|
2993
|
+
// ⚠️ Les noms suivent la convention existante : `list` / `list.all`, où le suffixe dit
|
|
2994
|
+
// « au-delà des miennes ». Ils sont documentés dans docs/HOST-CONTRACT.md, et une garde
|
|
2995
|
+
// vérifie que tout nom demandé ici y figure — une action inconnue vaut refus chez un hôte
|
|
2996
|
+
// prudent, donc l'introduire en silence casserait son instance sans rien dire.
|
|
2997
|
+
//
|
|
2998
|
+
// Le droit est demandé PARESSEUSEMENT : un propriétaire n'a pas à payer un aller-retour
|
|
2999
|
+
// pour lire ce qui lui appartient déjà.
|
|
3000
|
+
else if (body.action === "present-stats") {
|
|
3001
|
+
r = await presentationStats(String(body.slug || ""), u.email, isAdmin,
|
|
3002
|
+
() => PLAYER.identity.canManageShares(u, "presentations.stats"));
|
|
3003
|
+
}
|
|
3004
|
+
else if (body.action === "present-doc-list") {
|
|
3005
|
+
r = { ok: true, presentations: await listPresentationsForDoc(String(body.docId || ""), u.email, isAdmin,
|
|
3006
|
+
() => PLAYER.identity.canManageShares(u, "presentations.list.all")) };
|
|
3007
|
+
}
|
|
2943
3008
|
else if (body.action === "present-switch") {
|
|
2944
3009
|
if (!isAllowedStorageUrl(String(body.fileUrl || ""))) return jp(400, { ok: false, error: "url" });
|
|
2945
3010
|
r = await switchPresentationDoc(String(body.slug || ""), u.email, isAdmin, { fileUrl: body.fileUrl, fileName: body.fileName, docTitle: body.docTitle, docId: body.docId });
|
|
@@ -3444,23 +3509,38 @@ async function handler(req, res) {
|
|
|
3444
3509
|
return;
|
|
3445
3510
|
}
|
|
3446
3511
|
}
|
|
3447
|
-
const pres = await getPresentation(String(q.present));
|
|
3448
|
-
if (!pres) return sendRefusal(res, "ended", embed);
|
|
3449
3512
|
// Historique du chat (chargé au join par présentateur ET audience).
|
|
3450
3513
|
// ÉTAT de la présentation, relu par l'audience à la reconnexion ou au retour d'onglet.
|
|
3451
3514
|
// C'est la porte qui permettra de retirer la lecture anonyme des tables : l'audience n'a
|
|
3452
3515
|
// plus besoin de lire `doc_presentations` pour connaître la page courante. On ne renvoie
|
|
3453
3516
|
// QUE ce que l'audience doit savoir — ni propriétaire, ni jeton, ni horodatages internes.
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3517
|
+
// ⚠️ LA RÈGLE, PAS LA LISTE — ET C'EST LE SECOND HÔTE QUI A EU RAISON.
|
|
3518
|
+
//
|
|
3519
|
+
// La première version mettait en cache DEUX ROUTES ÉNUMÉRÉES. Leur remarque : « la troisième
|
|
3520
|
+
// route de lecture arrivera sans cache, et ça ne se verra pas ». C'est le motif croisé six
|
|
3521
|
+
// fois cette semaine — une liste se périme, une forme non.
|
|
3522
|
+
//
|
|
3523
|
+
// La règle est donc écrite ici, et le code la fait respecter par construction : TOUTE RÉPONSE
|
|
3524
|
+
// IDENTIQUE POUR TOUS LES SPECTATEURS D'UN MÊME SLUG SE CACHE. Ajouter une lecture publique,
|
|
3525
|
+
// c'est ajouter une entrée à cette table — elle sera mise en cache sans qu'on y pense, parce
|
|
3526
|
+
// qu'il n'existe pas d'autre chemin pour la servir.
|
|
3527
|
+
//
|
|
3528
|
+
// ⚠️ LA CONDITION D'ADMISSION EST « IDENTIQUE POUR TOUS », PAS « EN LECTURE SEULE ». Une
|
|
3529
|
+
// réponse qui dépendrait de l'appelant servirait l'état d'un visiteur à un autre — défaut
|
|
3530
|
+
// d'une tout autre gravité que celui qu'on ferme. Elle se revérifie à chaque champ ajouté.
|
|
3531
|
+
//
|
|
3532
|
+
// Le cache englobe la LECTURE ET la sérialisation : sous rafale, une seule des deux se paie.
|
|
3533
|
+
// `file=1` ne figure pas dans cette table, et c'est délibéré : il relaie des octets avec des
|
|
3534
|
+
// en-têtes de plage propres à l'appelant — ce n'est pas une charge partagée.
|
|
3535
|
+
const LECTURES_PARTAGEES = [
|
|
3536
|
+
{ param: "state", produire: async (p) => ({ ok: true, state: {
|
|
3537
|
+
active: p.active !== false,
|
|
3538
|
+
current_page: p.current_page || 1,
|
|
3539
|
+
content: p.content || null,
|
|
3540
|
+
file_url: p.file_url || null,
|
|
3541
|
+
file_name: p.file_name || null,
|
|
3542
|
+
doc_title: p.doc_title || null,
|
|
3543
|
+
updated_at: p.updated_at || null,
|
|
3464
3544
|
// ⚠️ CE CHAMP A PORTÉ `presenter_key` PENDANT UNE JOURNÉE, ET C'ÉTAIT DEUX FAUTES.
|
|
3465
3545
|
//
|
|
3466
3546
|
// 1. FUITE. `attendeeKey()` renvoie l'ADRESSE E-MAIL quand le participant en a une. La clé
|
|
@@ -3480,15 +3560,28 @@ async function handler(req, res) {
|
|
|
3480
3560
|
// liste des participants — laquelle ne porte plus aucun titre.
|
|
3481
3561
|
//
|
|
3482
3562
|
// Signalé par la seconde passe d'audit (P0-4) ; la fuite n'y était pas.
|
|
3483
|
-
presenter_name: String(
|
|
3484
|
-
} })
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3563
|
+
presenter_name: String(p.presenter_name || "") || null,
|
|
3564
|
+
} }) },
|
|
3565
|
+
{ param: "chat", produire: async (p, slug) => ({ ok: true, messages: await listMessages(slug), locked: !!p.chat_locked }) },
|
|
3566
|
+
];
|
|
3567
|
+
for (const lecture of LECTURES_PARTAGEES) {
|
|
3568
|
+
if (String(q[lecture.param] || "") !== "1") continue;
|
|
3569
|
+
const slugLu = String(q.present);
|
|
3570
|
+
const corps = await cacheLecture.lire(`${lecture.param}:${slugLu}`, async () => {
|
|
3571
|
+
const p = await getPresentation(slugLu);
|
|
3572
|
+
return p ? JSON.stringify(await lecture.produire(p, slugLu)) : null;
|
|
3573
|
+
});
|
|
3574
|
+
if (!corps) return sendRefusal(res, "ended", embed);
|
|
3488
3575
|
res.statusCode = 200; res.setHeader("Content-Type", "application/json"); res.setHeader("Cache-Control", "no-store");
|
|
3489
|
-
res.end(
|
|
3576
|
+
res.end(corps);
|
|
3490
3577
|
return;
|
|
3491
3578
|
}
|
|
3579
|
+
// ⚠️ LA LIGNE N'EST LUE QU'ICI, ET C'EST CE QUI DONNE SON SENS AU CACHE. Elle était lue
|
|
3580
|
+
// PLUS HAUT, avant toutes les branches : les deux chemins mis en cache la relisaient donc
|
|
3581
|
+
// quand même — le cache ne retirait rien, et il ajoutait une seconde interrogation. Une
|
|
3582
|
+
// mémoire posée derrière une dépense ne l'épargne pas.
|
|
3583
|
+
const pres = await getPresentation(String(q.present));
|
|
3584
|
+
if (!pres) return sendRefusal(res, "ended", embed);
|
|
3492
3585
|
if (String(q.file || "") === "1") {
|
|
3493
3586
|
if (!isAllowedStorageUrl(pres.file_url)) { res.statusCode = 404; res.end("Fichier indisponible"); return; }
|
|
3494
3587
|
const range = req.headers["range"];
|
package/server/presentations.js
CHANGED
|
@@ -341,10 +341,37 @@ async function recordAttendance(slug, { key, name, email, avatar, isMember, isPr
|
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
// Détail d'une présentation : entête + participants + nombre de messages par participant.
|
|
344
|
-
|
|
344
|
+
/**
|
|
345
|
+
* Les statistiques d'une présentation : qui a suivi, combien de temps, quelles pages.
|
|
346
|
+
*
|
|
347
|
+
* ⚠️ UNE SESSION NE SUFFIT PAS, ET C'EST LE CORRECTIF. Cette route exigeait un jeton et rien de plus :
|
|
348
|
+
* tout membre connaissant un slug lisait donc les participants d'une présentation qui n'était pas la
|
|
349
|
+
* sienne — leurs NOMS, leurs ADRESSES, leur temps de présence et les pages qu'ils ont vues. Ces
|
|
350
|
+
* participants sont souvent des prospects : ce sont des données commerciales sur des clients, pas un
|
|
351
|
+
* compteur d'usage.
|
|
352
|
+
*
|
|
353
|
+
* ⚠️ QUI Y A DROIT AU-DELÀ DU PROPRIÉTAIRE EST UNE RÈGLE DE L'HÔTE, PAS DU PLAYER. Une petite équipe
|
|
354
|
+
* où chacun voit tout est un choix parfaitement défendable ; une instance à plusieurs populations ne
|
|
355
|
+
* peut pas se le permettre. Le player ne tranche donc pas : il accorde ce qui est manifeste — le
|
|
356
|
+
* propriétaire, l'administrateur — et demande le reste à l'hôte, par le même crochet qui décide déjà
|
|
357
|
+
* qui peut diffuser un document.
|
|
358
|
+
*
|
|
359
|
+
* `autoriseLarge` est une FONCTION, pas un booléen : sans elle on paierait un aller-retour
|
|
360
|
+
* d'autorisation pour un propriétaire qui n'en a pas besoin.
|
|
361
|
+
*
|
|
362
|
+
* Signalé par deux audits (C-8, V-6).
|
|
363
|
+
*/
|
|
364
|
+
async function presentationStats(slug, email, isAdmin, autoriseLarge) {
|
|
345
365
|
if (!slug) return { ok: false, status: 400 };
|
|
346
366
|
const pres = await getPresentation(slug);
|
|
347
367
|
if (!pres) return { ok: false, status: 404 };
|
|
368
|
+
const proprietaire = !!isAdmin || !!(pres.owner_email && pres.owner_email === lc(email));
|
|
369
|
+
if (!proprietaire) {
|
|
370
|
+
let large = false;
|
|
371
|
+
// Une autorisation qui LÈVE vaut refus : « large » garde sa valeur initiale, et c'est la bonne.
|
|
372
|
+
try { large = typeof autoriseLarge === "function" ? !!(await autoriseLarge()) : !!autoriseLarge; } catch { /* hôte injoignable : refus */ }
|
|
373
|
+
if (!large) return { ok: false, status: 403 };
|
|
374
|
+
}
|
|
348
375
|
const [attRows, msgRows] = await Promise.all([
|
|
349
376
|
PLAYER.db.request(`doc_presentation_attendees?slug=eq.${enc(slug)}&select=*&order=first_seen.asc&limit=500`),
|
|
350
377
|
PLAYER.db.request(`doc_presentation_messages?slug=eq.${enc(slug)}&deleted=eq.false&select=author_email,author_name&limit=1000`),
|
|
@@ -441,10 +468,26 @@ async function setPresentationContent(slug, email, isAdmin, content, control) {
|
|
|
441
468
|
}
|
|
442
469
|
|
|
443
470
|
// Historique des présentations d'un document (pour l'onglet Suivi) : la plus récente d'abord, avec le nb de participants.
|
|
444
|
-
|
|
471
|
+
/**
|
|
472
|
+
* L'historique des présentations d'un document.
|
|
473
|
+
*
|
|
474
|
+
* ⚠️ ON FILTRE, ON NE REFUSE PAS — et la nuance est un choix. Refuser en bloc priverait un membre de
|
|
475
|
+
* SA propre liste dès qu'il n'a pas le droit élargi ; filtrer lui rend exactement ce qui lui revient.
|
|
476
|
+
* Une liste qui montre moins n'est pas une panne, une liste qui refuse tout en est une.
|
|
477
|
+
*
|
|
478
|
+
* Même règle que pour les statistiques : le propriétaire et l'administrateur toujours, le reste sur
|
|
479
|
+
* décision de l'hôte.
|
|
480
|
+
*/
|
|
481
|
+
async function listPresentationsForDoc(docId, email, isAdmin, autoriseLarge) {
|
|
445
482
|
if (!docId) return [];
|
|
446
|
-
const rows = await PLAYER.db.request(`doc_presentations?doc_id=eq.${enc(String(docId))}&select=slug,presenter_name,owner_name,current_page,active,created_at,updated_at&order=created_at.desc&limit=50`);
|
|
447
|
-
|
|
483
|
+
const rows = await PLAYER.db.request(`doc_presentations?doc_id=eq.${enc(String(docId))}&select=slug,presenter_name,owner_name,owner_email,current_page,active,created_at,updated_at&order=created_at.desc&limit=50`);
|
|
484
|
+
let list = Array.isArray(rows) ? rows : [];
|
|
485
|
+
if (!isAdmin) {
|
|
486
|
+
let large = false;
|
|
487
|
+
// Une autorisation qui LÈVE vaut refus : « large » garde sa valeur initiale, et c'est la bonne.
|
|
488
|
+
try { large = typeof autoriseLarge === "function" ? !!(await autoriseLarge()) : !!autoriseLarge; } catch { /* hôte injoignable : refus */ }
|
|
489
|
+
if (!large) list = list.filter((p) => p.owner_email && p.owner_email === lc(email));
|
|
490
|
+
}
|
|
448
491
|
// UNE requête groupée (in.(…)) au lieu d'une par présentation (N+1, jusqu'à 50) ; agrégation en mémoire.
|
|
449
492
|
const counts = new Map();
|
|
450
493
|
if (list.length) {
|
|
@@ -29,9 +29,12 @@ __export(shared_exports, {
|
|
|
29
29
|
DEFAULT_ZOOM: () => DEFAULT_ZOOM,
|
|
30
30
|
MAP_TYPES: () => MAP_TYPES,
|
|
31
31
|
PRESENTER_ACTIONS_PER_HOUR: () => PRESENTER_ACTIONS_PER_HOUR,
|
|
32
|
+
PRESENT_CACHE_MS: () => PRESENT_CACHE_MS,
|
|
33
|
+
PRESENT_QUOTA_MARGIN: () => PRESENT_QUOTA_MARGIN,
|
|
32
34
|
PRESENT_QUOTA_PER_HOUR: () => PRESENT_QUOTA_PER_HOUR,
|
|
33
35
|
PRESENT_READS_PER_HOUR: () => PRESENT_READS_PER_HOUR,
|
|
34
36
|
PRESENT_READ_BURST: () => PRESENT_READ_BURST,
|
|
37
|
+
PRESENT_READ_COALESCE_MS: () => PRESENT_READ_COALESCE_MS,
|
|
35
38
|
PRESENT_RESYNC_MS: () => PRESENT_RESYNC_MS,
|
|
36
39
|
PRESENT_SIGNAL_BUDGET_PER_HOUR: () => PRESENT_SIGNAL_BUDGET_PER_HOUR,
|
|
37
40
|
PRESENT_WRITE_TIMEOUT_MS: () => PRESENT_WRITE_TIMEOUT_MS,
|
|
@@ -109,17 +112,23 @@ var PRESENTER_ACTIONS_PER_HOUR = 720;
|
|
|
109
112
|
var PRESENT_READS_PER_HOUR = RESYNC_READS_PER_HOUR + PRESENTER_ACTIONS_PER_HOUR;
|
|
110
113
|
var PRESENT_SIGNAL_BUDGET_PER_HOUR = PRESENTER_ACTIONS_PER_HOUR;
|
|
111
114
|
var PRESENT_READ_BURST = 20;
|
|
112
|
-
var PRESENT_QUOTA_PER_HOUR = (PRESENT_READS_PER_HOUR + PRESENT_READ_BURST) * READERS_PER_EGRESS;
|
|
113
115
|
var PRESENT_WRITE_TIMEOUT_MS = 1e4;
|
|
116
|
+
var PRESENT_READ_COALESCE_MS = 400;
|
|
117
|
+
var PRESENT_CACHE_MS = PRESENT_READ_COALESCE_MS;
|
|
118
|
+
var PRESENT_QUOTA_MARGIN = 4;
|
|
119
|
+
var PRESENT_QUOTA_PER_HOUR = (PRESENT_READS_PER_HOUR + PRESENT_READ_BURST) * READERS_PER_EGRESS * PRESENT_QUOTA_MARGIN;
|
|
114
120
|
// Annotate the CommonJS export names for ESM import in node:
|
|
115
121
|
0 && (module.exports = {
|
|
116
122
|
DEFAULT_CENTER,
|
|
117
123
|
DEFAULT_ZOOM,
|
|
118
124
|
MAP_TYPES,
|
|
119
125
|
PRESENTER_ACTIONS_PER_HOUR,
|
|
126
|
+
PRESENT_CACHE_MS,
|
|
127
|
+
PRESENT_QUOTA_MARGIN,
|
|
120
128
|
PRESENT_QUOTA_PER_HOUR,
|
|
121
129
|
PRESENT_READS_PER_HOUR,
|
|
122
130
|
PRESENT_READ_BURST,
|
|
131
|
+
PRESENT_READ_COALESCE_MS,
|
|
123
132
|
PRESENT_RESYNC_MS,
|
|
124
133
|
PRESENT_SIGNAL_BUDGET_PER_HOUR,
|
|
125
134
|
PRESENT_WRITE_TIMEOUT_MS,
|