discovery-media-player 0.1.59 → 0.1.60

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.
@@ -63,7 +63,14 @@ function creerDb(env) {
63
63
  },
64
64
  body: options.body ? JSON.stringify(options.body) : undefined,
65
65
  });
66
- if (!r.ok) throw new Error(`Supabase ${methode} ${chemin} → ${r.status}`);
66
+ if (!r.ok) {
67
+ // ⚠️ LE CORPS DIT POURQUOI, LE CODE NE DIT QUE COMBIEN. Un « 400 » nu a coûté un aller-retour
68
+ // de forge complet pour apprendre ce que PostgREST avait écrit dans sa réponse depuis le
69
+ // début. Tronqué : c'est un message d'erreur, pas un relais de données.
70
+ let detail = "";
71
+ try { detail = (await r.text()).slice(0, 300); } catch { /* le statut restera seul */ }
72
+ throw new Error(`Supabase ${methode} ${chemin} → ${r.status}${detail ? ` — ${detail}` : ""}`);
73
+ }
67
74
  const texte = await r.text();
68
75
  return texte ? JSON.parse(texte) : null;
69
76
  }
@@ -173,6 +173,15 @@ async function lireDepuis(fh, stat, cible, range) {
173
173
  statut = 206;
174
174
  }
175
175
 
176
+ // ⚠️ LE PLAFOND AVANT L'ALLOCATION — parce que c'est l'allocation qui coûte. Le contrôle du
177
+ // relais (Content-Length, dans le handler) arrivait APRÈS ce Buffer.alloc : un fichier local plus
178
+ // grand que le plafond était alloué EN ENTIER à chaque requête, puis refusé — le refus payait
179
+ // exactement le prix qu'il devait éviter. Lu à l'appel et non au chargement : le serveur autonome
180
+ // fabrique son contexte à l'import, avant que l'exploitant ait pu poser la variable.
181
+ const plafond = Number(process.env.PLAYER_MAX_RELAY_BYTES || 0) || 60 * 1024 * 1024;
182
+ if (fin - debut + 1 > plafond) {
183
+ return reponse(413, {}, Buffer.alloc(0));
184
+ }
176
185
  const buf = Buffer.alloc(fin - debut + 1);
177
186
  const { bytesRead } = await fh.read(buf, 0, buf.length, debut);
178
187
  const corps = bytesRead === buf.length ? buf : buf.subarray(0, bytesRead);
@@ -32,7 +32,7 @@ need.
32
32
  "hostShare": true,
33
33
  "hostMail": true,
34
34
  "plugins": { "bot": false, "visitors": false, "brandIntro": false, "botBrowser": false, "providerQuotas": false },
35
- "schema": { "attendues": 3, "sondees": 1, "verdict": "partiel", "manquant": [] }
35
+ "schema": { "couvre": "colonnes-conditionnelles", "attendues": 4, "sondees": 1, "verdict": "partiel", "manquant": [] }
36
36
  }
37
37
  ```
38
38
 
@@ -70,6 +70,13 @@ it. `verdict` is then one of:
70
70
  ⚠️ **`incomplet` wins over `partiel`**: a missing column is a positive fact and settles the verdict
71
71
  on its own, even when the rest has not been checked.
72
72
 
73
+ ⚠️ **`couvre` states the scope, because `complet` without a scope overpromises.** The card checks
74
+ the **conditional columns** — the ones a feature probes before writing. The rate-limit migrations
75
+ (`0003`, `0004`) are deliberately **not** in it: a host may provide its own `limits` capability, and
76
+ on such a host their absence is normal, not a defect. `complet` therefore never means *every file
77
+ under `supabase/` has been applied* — read it as *every column this code conditions its writes on
78
+ is present*.
79
+
73
80
  ⚠️ **`indetermine` is not a failure of the card.** A control column — the primary key of the oldest
74
81
  table — is queried first. If *it* does not answer, nothing is missing: the database is. Without that
75
82
  control, an unreachable database would make all three probes fail and the card would announce three
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
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,He=Object.defineProperties,De=Object.getOwnPropertyDescriptor,We=Object.getOwnPropertyDescriptors,Be=Object.getOwnPropertyNames,he=Object.getOwnPropertySymbols;var we=Object.prototype.hasOwnProperty,Ve=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,G=(e,n)=>{for(var t in n||(n={}))we.call(n,t)&&ve(e,t,n[t]);if(he)for(var t of he(n))Ve.call(n,t)&&ve(e,t,n[t]);return e},Me=(e,n)=>He(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 Be(n))!we.call(e,o)&&o!==t&&C(e,o,{get:()=>n[o],enumerable:!(r=De(n,o))||r.enumerable});return e};var Fe=e=>ze(C({},\"__esModule\",{value:!0}),e);var Gn={};v(Gn,{bridge:()=>L,cadence:()=>N,chat:()=>$,live:()=>U,presentation:()=>H,presentationState:()=>W,tracking:()=>O,viewer:()=>D});var L={};v(L,{onHostMessage:()=>en,onPlayerMessage:()=>Je,parseHostMessage:()=>_e,parsePlayerMessage:()=>Te,sendToHost:()=>qe,sendToPlayer:()=>Qe,toWire:()=>Y});var K=\"3dd-doc-\",je=/^[A-Za-z0-9_-]{1,64}$/,Ge=[\"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 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(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 s=e.slug;return typeof s!=\"string\"||!je.test(s)?null:{type:o,slug:s}}function Te(e){return xe(e,Ge,Ye)}function _e(e){return xe(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=s=>{if(!Xe(t,s.source))return;let a=Te(s.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function en(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 u=_e(a.data);u&&e(u)};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:()=>Ie,PRESENT_QUOTA_PER_HOUR:()=>on,PRESENT_READS_PER_HOUR:()=>Re,PRESENT_READ_BURST:()=>Pe,PRESENT_READ_COALESCE_MS:()=>Ae,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,Pe=20,J=1e4,Ae=400,rn=Ae,Ie=4,on=(Re+Pe)*q*Ie;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\",s=(ge=e.idleMs)!=null?ge:Q,a=(be=e.sessionEveryMs)!=null?be:12e3,u=e.slug||\"\",d=e.internal||null,m=e.sessionId||an(),p=e.send||un(o,n),c={},l=0,g=0,f=0,w=0,h=null,E=r(),S=!1,B=!1,A=[],$e=()=>!!u||!!d,ce=i=>{if($e())try{p(i)}catch(b){}},V=()=>!!t&&t.visibilityState===\"visible\"&&!S,de=i=>{let b=r()-i,x=Math.max(0,E-i)+s;return Math.max(0,Math.min(b,x))},z=()=>{h!=null&&l>0&&(c[l]=(c[l]||0)+de(h)/1e3),h=V()?r():null},pe=()=>{z(),h=null},F=()=>{h==null&&V()&&(h=r())},j=()=>{let i=0;for(let b in c)i+=c[b];return h!=null&&(i+=de(h)/1e3),Math.round(i)},fe=()=>{let i={};for(let b in c)i[b]=Math.round(c[b]);return i},k=()=>{E=r(),S&&(S=!1,F())},me=(i,b)=>{d||ce({slug:u,event:i,page:b||g,maxPage:f,seconds:j(),sessionId:m})},I=()=>{z();let i={event:\"session\",sessionId:m,numPages:w,maxPage:f,totalSeconds:j(),pagesTime:fe()};d?(i.internal=!0,i.docId=d.docId,i.email=d.email,i.name=d.name,d.it&&(i.it=d.it)):i.slug=u,ce(i)},R=(i,b,x,ye)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(b,x,ye),A.push(()=>{i.removeEventListener(b,x,ye)}))};return{sessionId:m,totalSeconds:j,maxPage:()=>f,pageTimes:fe,start(){if(B)return;B=!0,me(\"open\",1),l=1,E=r(),h=V()?r():null,R(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(pe(),I()):(k(),F())}),R(n,\"focus\",()=>{k(),F()});for(let x of sn)R(n,x,k,{passive:!0});R(e.scrollElement,\"scroll\",k,{passive:!0}),R(n,\"beforeunload\",I);let i=n.setInterval(()=>{!S&&r()-E>s&&(S=!0,pe())},5e3),b=n.setInterval(I,a);A.push(()=>{n.clearInterval(i),n.clearInterval(b)})},setPage(i){!i||i===g||(E=r(),z(),l=i,g=i,i>f&&(f=i,me(\"page\",i)))},setPageCount(i){w=i||0},noteActivity:k,flush:I,stop(){for(;A.length;){let i=A.pop();try{i==null||i()}catch(b){}}B=!1}}}var U={};v(U,{STORAGE_KEYS:()=>M,attendeeKey:()=>fn,authorToken:()=>mn,avatarHtml:()=>ee,canModerate:()=>oe,createBudget:()=>Mn,createFileEcritures:()=>xn,createScheduler:()=>vn,escapeHtml:()=>y,fetchBorne:()=>wn,flattenPresence:()=>cn,formatMessageBody:()=>ne,initials:()=>Ce,isMentioned:()=>bn,isMine:()=>T,presenceId:()=>Tn,reactorId:()=>re,referenceAuteur:()=>gn,shouldNotify:()=>yn,unreadLabel:()=>hn});var M={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\",presenceId:\"3dd-present-uid\"};function y(e){return String(e==null?\"\":e).replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\").replace(/\"/g,\"&quot;\")}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=\"${y(e)}\" alt=\"\">`:y(Ce(n))}function ne(e){let n=y(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=new Map;for(let t in e||{})for(let r of(e||{})[t]||[]){let o=String(r.uid||r.email||r.name||\"\").toLowerCase()||`_${t}_${n.size}`;(!n.get(o)||r.role===\"presenter\")&&n.set(o,r)}return[...n.values()]}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(M.attendeeKey);if(r)return r;let o=`anon-${t()}`;return e.setItem(M.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,M.authorToken);if(t)return t;let r=n();return pn(e,M.authorToken,r),r}async function gn(e,n){let t=n!==void 0?n:typeof globalThis!=\"undefined\"&&globalThis.crypto?globalThis.crypto.subtle:null;if(!t||!e)return\"\";try{let r=async o=>{let s=await t.digest(\"SHA-256\",new TextEncoder().encode(o));return Array.from(new Uint8Array(s),a=>a.toString(16).padStart(2,\"0\")).join(\"\")};return(await r(\"ref:\"+await r(e))).slice(0,16)}catch(r){return\"\"}}function re(e){return String(e&&e.ref||\"\")}function T(e,n){return!e||!n||!e.author_ref||!n.ref?!1:e.author_ref===n.ref}function oe(e){return!!(e&&e.role===\"presenter\")}function bn(e,n){if(!n||!n.name)return!1;let t=(n.name.split(/\\s+/)[0]||\"\").toLowerCase();return t?String(e&&e.body||\"\").toLowerCase().includes(`@${t}`)&&!T(e,n):!1}function yn(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||T(n,t)?!1:o}function hn(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function vn(e,n={}){var g;let t=(g=n.minMs)!=null?g:300,r=n.now||(()=>Date.now()),o=n.setTimer||((f,w)=>setTimeout(f,w)),s=n.clearTimer||(f=>clearTimeout(f)),a=!1,u=!1,d=-1/0,m=null,p=!1;function c(){if(p||a){u=!0;return}let f=t-(r()-d);if(f>0){l(f);return}a=!0,u=!1,d=r();let w=!1;e(()=>{w||(w=!0,a=!1,u&&!p&&c())})}function l(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&&(s(m),m=null)}}}function wn(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||((l,g)=>setTimeout(l,g)),a=r.clearTimer||(l=>clearTimeout(l)),u=null;try{u=new AbortController}catch(l){}let d=u?Me(G({},n||{}),{signal:u.signal}):G({},n||{}),m=null,p=new Promise((l,g)=>{m=s(()=>{try{u==null||u.abort()}catch(f){}g(new Error(\"delai\"))},t)}),c=()=>{try{a(m)}catch(l){}};return Promise.race([o(e,d),p]).then(l=>(c(),l),l=>{throw c(),l})}function Mn(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}}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,s=!1,a=-1/0,u=!1;function d(){if(s||o.size===0)return;let p=n-(t()-a);if(p>0){if(u)return;u=!0,r(()=>{u=!1,d()},p);return}let c=o.keys().next().value,l=o.get(c);o.delete(c),s=!0,a=t(),Promise.resolve().then(l.tache).then(g=>l.regler(g),()=>l.regler(null)).then(()=>{s=!1,d()})}return{poser(p,c){return new Promise(l=>{let g=o.get(p);o.set(p,{tache:c,regler:f=>{g&&g.regler(f),l(f)}}),d()})},enAttente:()=>o.size}}function Tn(e,n=te){try{if(e){let t=e.getItem(M.presenceId);if(t)return t;let r=`p-${n()}`;return e.setItem(M.presenceId,r),r}}catch(t){}return`p-${n()}`}var $={};v($,{messageClassName:()=>En,renderActions:()=>Oe,renderAttachment:()=>Ne,renderMessage:()=>_n,renderReactions:()=>Le});function Le(e,n){let t=e&&e.reactions||{},r=re(n),o=\"\";for(let s in t){let a=t[s]||[];if(!a.length)continue;let u=a.indexOf(r)>=0;o+=`<button class=\"re-chip${u?\" on\":\"\"}\" data-e=\"${y(s)}\">${y(s)} ${a.length}</button>`}return o}function Ne(e){if(!e||!e.url)return\"\";let n=y(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=y(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=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||oe(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function _n(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>${y(e.reply_name||\"\")}</b> ${y(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>${y(e.author_name||\"Invit\\xE9\")}</b>${s}</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 En(e,n,t){return\"cm\"+(T(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var H={};v(H,{DEFAULT_CENTER:()=>ae,DEFAULT_ZOOM:()=>ue,MAP_TYPES:()=>P,cycleMapType:()=>Sn,initialMapContent:()=>Rn,mapTypeLabel:()=>kn,sanitizeContent:()=>le});var P=[\"roadmap\",\"satellite\",\"hybrid\"],ae=[46.6,2.5],ue=6;function _(e){let n=Number(e);return Number.isFinite(n)?n:null}function se(e){if(!Array.isArray(e))return null;let n=_(e[0]),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=se(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:_(r.heading)||0,pitch:ie(_(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:ie(_(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:se(n.center)||[...ae],zoom:ie(Math.trunc(_(n.zoom)||ue),1,21),marker:se(n.marker),mapType:P.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function Sn(e){let n=P.indexOf(e);return P[(n+1)%P.length]}function kn(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function Rn(){return{kind:\"map\",center:[...ae],zoom:ue,marker:null,mapType:null,label:null}}var D={};v(D,{CURRENT_PAGE_MARGIN:()=>Nn,DEFAULT_ASPECT:()=>In,MAX_ZOOM:()=>An,MIN_PAGE_WIDTH:()=>Cn,MIN_ZOOM:()=>Pn,PRERENDER_MARGIN:()=>Ln,arrowState:()=>$n,averageColor:()=>Wn,clampPage:()=>Ue,clampZoom:()=>On,fitWidth:()=>Dn,isImageDocument:()=>Bn,progressPercent:()=>Un,workerBlobUrl:()=>Vn});var Pn=.5,An=3,In=1.35,Cn=280,Ln=\"500px 0px\",Nn=\"-48% 0px -48% 0px\";function On(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function Ue(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function Un(e,n){return n?Math.round(Ue(e,n)/n*100):0}function $n(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var Hn=28;function Dn(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)-Hn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function Wn(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||\"\"))}async function Vn(e,n,t={}){let r=t.fetch||(typeof fetch==\"function\"?(a=>fetch(a)):null),o=t.subtle!==void 0?t.subtle:typeof globalThis!=\"undefined\"&&globalThis.crypto?globalThis.crypto.subtle:null,s=t.creerUrl||(a=>URL.createObjectURL(a));if(!r||!o||!n)return null;try{let a=await r(e);if(!a.ok)return null;let u=await a.arrayBuffer(),d=await o.digest(\"SHA-384\",u);return`sha384-${zn(new Uint8Array(d))}`!==n?null:s(new Blob([u],{type:\"application/javascript\"}))}catch(a){return null}}function zn(e){let n=\"\";for(let t of e)n+=String.fromCharCode(t);return btoa(n)}var W={};v(W,{presentationTransition:()=>Fn,switchDocUrl:()=>jn});function Fn(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 jn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return Fe(Gn);})();", SOURCE_HASH: "5bacfcb0f5b69825" };
7
+ module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var C=Object.defineProperty,He=Object.defineProperties,De=Object.getOwnPropertyDescriptor,We=Object.getOwnPropertyDescriptors,Be=Object.getOwnPropertyNames,he=Object.getOwnPropertySymbols;var we=Object.prototype.hasOwnProperty,Ve=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,G=(e,n)=>{for(var t in n||(n={}))we.call(n,t)&&ve(e,t,n[t]);if(he)for(var t of he(n))Ve.call(n,t)&&ve(e,t,n[t]);return e},Me=(e,n)=>He(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 Be(n))!we.call(e,o)&&o!==t&&C(e,o,{get:()=>n[o],enumerable:!(r=De(n,o))||r.enumerable});return e};var Fe=e=>ze(C({},\"__esModule\",{value:!0}),e);var Gn={};v(Gn,{bridge:()=>L,cadence:()=>N,chat:()=>$,live:()=>U,presentation:()=>H,presentationState:()=>W,tracking:()=>O,viewer:()=>D});var L={};v(L,{onHostMessage:()=>en,onPlayerMessage:()=>Je,parseHostMessage:()=>_e,parsePlayerMessage:()=>Te,sendToHost:()=>qe,sendToPlayer:()=>Qe,toWire:()=>Y});var K=\"3dd-doc-\",je=/^[A-Za-z0-9_-]{1,64}$/,Ge=[\"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 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(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 s=e.slug;return typeof s!=\"string\"||!je.test(s)?null:{type:o,slug:s}}function Te(e){return xe(e,Ge,Ye)}function _e(e){return xe(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=s=>{if(!Xe(t,s.source))return;let a=Te(s.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function en(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 u=_e(a.data);u&&e(u)};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:()=>Ie,PRESENT_QUOTA_PER_HOUR:()=>on,PRESENT_READS_PER_HOUR:()=>Re,PRESENT_READ_BURST:()=>Pe,PRESENT_READ_COALESCE_MS:()=>Ae,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,Pe=20,J=1e4,Ae=400,rn=Ae,Ie=4,on=(Re+Pe)*q*Ie;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\",s=(ge=e.idleMs)!=null?ge:Q,a=(be=e.sessionEveryMs)!=null?be:12e3,u=e.slug||\"\",d=e.internal||null,m=e.sessionId||an(),p=e.send||un(o,n),c={},l=0,g=0,f=0,w=0,h=null,E=r(),S=!1,B=!1,A=[],$e=()=>!!u||!!d,ce=i=>{if($e())try{p(i)}catch(b){}},V=()=>!!t&&t.visibilityState===\"visible\"&&!S,de=i=>{let b=r()-i,x=Math.max(0,E-i)+s;return Math.max(0,Math.min(b,x))},z=()=>{h!=null&&l>0&&(c[l]=(c[l]||0)+de(h)/1e3),h=V()?r():null},pe=()=>{z(),h=null},F=()=>{h==null&&V()&&(h=r())},j=()=>{let i=0;for(let b in c)i+=c[b];return h!=null&&(i+=de(h)/1e3),Math.round(i)},fe=()=>{let i={};for(let b in c)i[b]=Math.round(c[b]);return i},k=()=>{E=r(),S&&(S=!1,F())},me=(i,b)=>{d||ce({slug:u,event:i,page:b||g,maxPage:f,seconds:j(),sessionId:m})},I=()=>{z();let i={event:\"session\",sessionId:m,numPages:w,maxPage:f,totalSeconds:j(),pagesTime:fe()};d?(i.internal=!0,i.docId=d.docId,i.email=d.email,i.name=d.name,d.it&&(i.it=d.it)):i.slug=u,ce(i)},R=(i,b,x,ye)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(b,x,ye),A.push(()=>{i.removeEventListener(b,x,ye)}))};return{sessionId:m,totalSeconds:j,maxPage:()=>f,pageTimes:fe,start(){if(B)return;B=!0,me(\"open\",1),l=1,E=r(),h=V()?r():null,R(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(pe(),I()):(k(),F())}),R(n,\"focus\",()=>{k(),F()});for(let x of sn)R(n,x,k,{passive:!0});R(e.scrollElement,\"scroll\",k,{passive:!0}),R(n,\"beforeunload\",I);let i=n.setInterval(()=>{!S&&r()-E>s&&(S=!0,pe())},5e3),b=n.setInterval(I,a);A.push(()=>{n.clearInterval(i),n.clearInterval(b)})},setPage(i){!i||i===g||(E=r(),z(),l=i,g=i,i>f&&(f=i,me(\"page\",i)))},setPageCount(i){w=i||0},noteActivity:k,flush:I,stop(){for(;A.length;){let i=A.pop();try{i==null||i()}catch(b){}}B=!1}}}var U={};v(U,{STORAGE_KEYS:()=>M,attendeeKey:()=>fn,authorToken:()=>mn,avatarHtml:()=>ee,canModerate:()=>oe,createBudget:()=>Mn,createFileEcritures:()=>xn,createScheduler:()=>vn,escapeHtml:()=>y,fetchBorne:()=>wn,flattenPresence:()=>cn,formatMessageBody:()=>ne,initials:()=>Ce,isMentioned:()=>bn,isMine:()=>T,presenceId:()=>Tn,reactorId:()=>re,referenceAuteur:()=>gn,shouldNotify:()=>yn,unreadLabel:()=>hn});var M={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\",presenceId:\"3dd-present-uid\"};function y(e){return String(e==null?\"\":e).replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\").replace(/\"/g,\"&quot;\")}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=\"${y(e)}\" alt=\"\">`:y(Ce(n))}function ne(e){let n=y(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=new Map;for(let t in e||{})for(let r of(e||{})[t]||[]){let o=String(r.uid||r.email||r.name||\"\").toLowerCase()||`_${t}_${n.size}`;(!n.get(o)||r.role===\"presenter\")&&n.set(o,r)}return[...n.values()]}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(M.attendeeKey);if(r)return r;let o=`anon-${t()}`;return e.setItem(M.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,M.authorToken);if(t)return t;let r=n();return pn(e,M.authorToken,r),r}async function gn(e,n){let t=n!==void 0?n:typeof globalThis!=\"undefined\"&&globalThis.crypto?globalThis.crypto.subtle:null;if(!t||!e)return\"\";try{let r=async o=>{let s=await t.digest(\"SHA-256\",new TextEncoder().encode(o));return Array.from(new Uint8Array(s),a=>a.toString(16).padStart(2,\"0\")).join(\"\")};return(await r(\"ref:\"+await r(e))).slice(0,16)}catch(r){return\"\"}}function re(e){return String(e&&e.ref||\"\")}function T(e,n){return!e||!n||!e.author_ref||!n.ref?!1:e.author_ref===n.ref}function oe(e){return!!(e&&e.role===\"presenter\")}function bn(e,n){if(!n||!n.name)return!1;let t=(n.name.split(/\\s+/)[0]||\"\").toLowerCase();return t?String(e&&e.body||\"\").toLowerCase().includes(`@${t}`)&&!T(e,n):!1}function yn(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||T(n,t)?!1:o}function hn(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function vn(e,n={}){var g;let t=(g=n.minMs)!=null?g:300,r=n.now||(()=>Date.now()),o=n.setTimer||((f,w)=>setTimeout(f,w)),s=n.clearTimer||(f=>clearTimeout(f)),a=!1,u=!1,d=-1/0,m=null,p=!1;function c(){if(p||a){u=!0;return}let f=t-(r()-d);if(f>0){l(f);return}a=!0,u=!1,d=r();let w=!1;e(()=>{w||(w=!0,a=!1,u&&!p&&c())})}function l(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&&(s(m),m=null)}}}function wn(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||((l,g)=>setTimeout(l,g)),a=r.clearTimer||(l=>clearTimeout(l)),u=null;try{u=new AbortController}catch(l){}let d=u?Me(G({},n||{}),{signal:u.signal}):G({},n||{}),m=null,p=new Promise((l,g)=>{m=s(()=>{try{u==null||u.abort()}catch(f){}g(new Error(\"delai\"))},t)}),c=()=>{try{a(m)}catch(l){}};return Promise.race([o(e,d),p]).then(l=>(c(),l),l=>{throw c(),l})}function Mn(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}}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,s=!1,a=-1/0,u=!1;function d(){if(s||o.size===0)return;let p=n-(t()-a);if(p>0){if(u)return;u=!0,r(()=>{u=!1,d()},p);return}let c=o.keys().next().value,l=o.get(c);o.delete(c),s=!0,a=t(),Promise.resolve().then(l.tache).then(g=>l.regler(g),()=>l.regler(null)).then(()=>{s=!1,d()})}return{poser(p,c){return new Promise(l=>{let g=o.get(p);o.set(p,{tache:c,regler:f=>{g&&g.regler(f),l(f)}}),d()})},enAttente:()=>o.size}}function Tn(e,n=te){try{if(e){let t=e.getItem(M.presenceId);if(t)return t;let r=`p-${n()}`;return e.setItem(M.presenceId,r),r}}catch(t){}return`p-${n()}`}var $={};v($,{messageClassName:()=>En,renderActions:()=>Oe,renderAttachment:()=>Ne,renderMessage:()=>_n,renderReactions:()=>Le});function Le(e,n){let t=e&&e.reactions||{},r=re(n),o=\"\";for(let s in t){let a=t[s]||[];if(!a.length)continue;let u=a.indexOf(r)>=0;o+=`<button class=\"re-chip${u?\" on\":\"\"}\" data-e=\"${y(s)}\">${y(s)} ${a.length}</button>`}return o}function Ne(e){if(!e||!e.url)return\"\";let n=y(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=y(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=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||oe(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function _n(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>${y(e.reply_name||\"\")}</b> ${y(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>${y(e.author_name||\"Invit\\xE9\")}</b>${s}</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 En(e,n,t){return\"cm\"+(T(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var H={};v(H,{DEFAULT_CENTER:()=>ae,DEFAULT_ZOOM:()=>ue,MAP_TYPES:()=>P,cycleMapType:()=>Sn,initialMapContent:()=>Rn,mapTypeLabel:()=>kn,sanitizeContent:()=>le});var P=[\"roadmap\",\"satellite\",\"hybrid\"],ae=[46.6,2.5],ue=6;function _(e){let n=Number(e);return Number.isFinite(n)?n:null}function se(e){if(!Array.isArray(e))return null;let n=_(e[0]),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=se(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:_(r.heading)||0,pitch:ie(_(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:ie(_(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:se(n.center)||[...ae],zoom:ie(Math.trunc(_(n.zoom)||ue),1,21),marker:se(n.marker),mapType:P.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function Sn(e){let n=P.indexOf(e);return P[(n+1)%P.length]}function kn(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function Rn(){return{kind:\"map\",center:[...ae],zoom:ue,marker:null,mapType:null,label:null}}var D={};v(D,{CURRENT_PAGE_MARGIN:()=>Nn,DEFAULT_ASPECT:()=>In,MAX_ZOOM:()=>An,MIN_PAGE_WIDTH:()=>Cn,MIN_ZOOM:()=>Pn,PRERENDER_MARGIN:()=>Ln,arrowState:()=>$n,averageColor:()=>Wn,clampPage:()=>Ue,clampZoom:()=>On,fitWidth:()=>Dn,isImageDocument:()=>Bn,progressPercent:()=>Un,workerBlobUrl:()=>Vn});var Pn=.5,An=3,In=1.35,Cn=280,Ln=\"500px 0px\",Nn=\"-48% 0px -48% 0px\";function On(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function Ue(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function Un(e,n){return n?Math.round(Ue(e,n)/n*100):0}function $n(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var Hn=28;function Dn(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)-Hn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function Wn(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||\"\"))}async function Vn(e,n,t={}){let r=t.fetch||(typeof fetch==\"function\"?(a=>fetch(a)):null),o=t.subtle!==void 0?t.subtle:typeof globalThis!=\"undefined\"&&globalThis.crypto?globalThis.crypto.subtle:null,s=t.creerUrl||(a=>URL.createObjectURL(a));if(!r||!o||!n)return null;try{let a=await r(e);if(!a.ok)return null;let u=await a.arrayBuffer(),d=await o.digest(\"SHA-384\",u);return`sha384-${zn(new Uint8Array(d))}`!==n?null:s(new Blob([u],{type:\"application/javascript\"}))}catch(a){return null}}function zn(e){let n=\"\";for(let t of e)n+=String.fromCharCode(t);return btoa(n)}var W={};v(W,{presentationTransition:()=>Fn,switchDocUrl:()=>jn});function Fn(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 jn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return Fe(Gn);})();", SOURCE_HASH: "0e1b2ed50563dde4" };
package/server/handler.js CHANGED
@@ -434,7 +434,7 @@ var Live=(function(){
434
434
  // s'éteindre, recommence, et rebascule encore. Rejouer la même intention deux fois donne le
435
435
  // même résultat qu'une fois ; c'est ce que le réseau exige.
436
436
  var _m=msgData[id]||{},_rs=(_m.reactions&&_m.reactions[e])||[],veut=_rs.indexOf(MOIREF)<0;
437
- fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'present-react',slug:SLUG,msgId:+id,emoji:e,reactor:MOIREF,etat:veut})}).then(majDiffusee).catch(function(){});}
437
+ fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'present-react',slug:SLUG,msgId:+id,emoji:e,authorToken:authToken(),etat:veut})}).then(majDiffusee).catch(function(){});}
438
438
  function setReply(id){var m=msgData[id];if(!m||m.deleted)return;var nm=m.author_name||'Invité';replyCtx={id:+id,name:nm,text:(m.body||'').slice(0,120)};var el=document.getElementById('chatReply');if(el){el.style.display='flex';el.innerHTML='<span class=cq><b>'+esc(nm)+'</b> '+esc((m.body||'').slice(0,80))+'</span><button id=chatReplyX title=Annuler>×</button>';var x=document.getElementById('chatReplyX');if(x)x.addEventListener('click',clearReply);}var t=document.getElementById('chatText');if(t)t.focus();}
439
439
  function clearReply(){replyCtx=null;var el=document.getElementById('chatReply');if(el){el.style.display='none';el.innerHTML='';}}
440
440
  function send(){var i=document.getElementById('chatText');var t=(i.value||'').trim();if(!t||!ME)return;if(LOCKED&&!canMod())return;i.value='';toggleSend();
@@ -1560,7 +1560,13 @@ const PLAFOND_RELAIS = Number(process.env.PLAYER_MAX_RELAY_BYTES || 0) || 60 * 1
1560
1560
 
1561
1561
  async function relayerFichier(res, r, disposition) {
1562
1562
  if (!r) { res.statusCode = 404; res.end("Fichier indisponible"); return; }
1563
- if (!r.ok && r.status !== 206) { res.statusCode = 502; res.end("Fichier indisponible"); return; }
1563
+ // 413 et 416 sont des REFUS ARGUMENTÉS de l'amont local (plafond, borne absurde) : les fondre
1564
+ // dans un 502 dirait « panne » là où l'amont a dit « demande irrecevable ».
1565
+ if (!r.ok && r.status !== 206) {
1566
+ res.statusCode = r.status === 413 || r.status === 416 ? r.status : 502;
1567
+ res.end(r.status === 413 ? "Fichier trop volumineux" : "Fichier indisponible");
1568
+ return;
1569
+ }
1564
1570
 
1565
1571
  const compresse = !!r.headers.get("content-encoding");
1566
1572
  if (compresse && r.status === 206) { res.statusCode = 502; res.end("Fichier indisponible"); return; }
@@ -3398,7 +3404,16 @@ async function handler(req, res) {
3398
3404
  const ip = adresseAppelant(req) || "anon";
3399
3405
  const allowed = await PLAYER.limits.allow(`preact:${ip}`, 200, 3600);
3400
3406
  if (!allowed) return jp(429, { ok: false, error: "rate" });
3401
- const r = await toggleReaction(String(body.slug || ""), body.msgId, body.emoji, body.reactor);
3407
+ // ⚠️ LE CINQUIÈME ARGUMENT A MANQUÉ ICI PENDANT TROIS VERSIONS. `toggleReaction` sait
3408
+ // poser un état depuis 0.1.56, le client l'envoie depuis 0.1.56 — et cette ligne le
3409
+ // jetait. Les essais éprouvaient la FONCTION, jamais la route : le correctif était vu
3410
+ // refuser en mutation et inactif en production. Trouvé par le troisième audit.
3411
+ // ⚠️ `body.reactor` N'EST PLUS LU. C'est un champ que le client CHOISIT, et les refs des
3412
+ // autres participants sont publics (le tableau des réactions les porte) : s'y fier
3413
+ // laissait n'importe qui poser ou retirer les réactions d'un autre. L'identité se
3414
+ // dérive du jeton d'auteur — le secret qui ne quitte pas le navigateur qui l'a tiré.
3415
+ const reacteur = require("./presentations").reacteurDepuisJeton(body.authorToken);
3416
+ const r = await toggleReaction(String(body.slug || ""), body.msgId, body.emoji, reacteur, body.etat);
3402
3417
  return jp(r.ok ? 200 : (r.status || 400), r);
3403
3418
  } catch { return jp(500, { ok: false }); }
3404
3419
  }
@@ -3832,9 +3847,14 @@ async function handler(req, res) {
3832
3847
  // carte est ce qu'un hôte interroge déjà pour épingler sa version ; c'est donc ici.
3833
3848
  // Elle RAPPORTE ce qui est connu, elle ne sonde pas : un diagnostic ne doit pas tomber en
3834
3849
  // même temps que ce qu'il diagnostique.
3835
- schema: String(q.schema || "") === "1"
3836
- ? await require("./schema").sonderTout()
3837
- : require("./schema").etatDuSchema(),
3850
+ // ⚠️ `couvre` DIT LA PORTÉE, parce que « complet » sans portée surpromet : les migrations de
3851
+ // limites (0003/0004) n'y sont PAS — un hôte peut fournir sa propre capacité `limits`, et
3852
+ // chez lui leur absence est normale. Ce champ empêche de lire « complet » comme « tout le
3853
+ // dossier supabase/ est appliqué ». Relevé par le troisième audit.
3854
+ schema: { couvre: "colonnes-conditionnelles",
3855
+ ...(String(q.schema || "") === "1"
3856
+ ? await require("./schema").sonderTout()
3857
+ : require("./schema").etatDuSchema()) },
3838
3858
  // « L'hôte peut-il créer un lien en son nom propre ? » — configuré, pas seulement
3839
3859
  // possible. Un hôte qui oublie le secret reçoit un 401 qui ressemble à un droit
3840
3860
  // manquant ; ce booléen le lui dit sans qu'il ait à essayer.
@@ -55,7 +55,15 @@ async function reclaimPresentation(slug, email) {
55
55
  // docs/MIGRATIONS.md décrit : PostgREST rejette le PATCH ENTIER si la colonne manque. La reprise
56
56
  // aurait cessé de fonctionner chez tout hôte non migré — pas la nouvelle garantie, la reprise.
57
57
  const rangDispo = await require("./schema").attendue("rangEcriture");
58
- await PLAYER.db.request(`doc_presentations?slug=eq.${enc(slug)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { control_hash: sha(control), active: true, ...(rangDispo ? { write_seq: 0 } : {}), last_seen: new Date().toISOString(), updated_at: new Date().toISOString() } });
58
+ // ⚠️ LE PROPRIÉTAIRE DANS LA CONDITION, pas seulement dans la vérification. Entre la lecture et
59
+ // l'écriture, un transfert a pu donner la présentation à quelqu'un d'autre : la reprise de
60
+ // l'ANCIEN propriétaire arrivait quand même, régénérait le jeton, et volait la session au
61
+ // nouveau — précisément ce que le transfert venait d'accorder. Constat du troisième audit.
62
+ const ecrit = await ecrireSiEncoreVrai(
63
+ `doc_presentations?slug=eq.${enc(slug)}&owner_email=eq.${enc(row.owner_email)}`,
64
+ { control_hash: sha(control), active: true, ...(rangDispo ? { write_seq: 0 } : {}), last_seen: new Date().toISOString(), updated_at: new Date().toISOString() },
65
+ );
66
+ if (!ecrit) return { ok: false, status: 409 };
59
67
  return { ok: true, slug, control, page: row.current_page || 1, fileUrl: row.file_url, fileName: row.file_name, docTitle: row.doc_title, docId: row.doc_id };
60
68
  }
61
69
 
@@ -65,8 +73,15 @@ async function touchPresentation(slug, control) {
65
73
  const row = await getPresentation(slug);
66
74
  if (!row) return { ok: false, status: 404 };
67
75
  if (!tokenMatches(control, row.control_hash)) return { ok: false, status: 403 };
68
- await PLAYER.db.request(`doc_presentations?slug=eq.${enc(slug)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { last_seen: new Date().toISOString() } });
69
- return { ok: true };
76
+ // ⚠️ LE JETON DANS LA CONDITION : une reprise ailleurs le régénère, une clôture l'annule. Sans
77
+ // ça, le heartbeat d'un ANCIEN onglet maintenait « vivante » une session qui ne lui appartient
78
+ // plus — et l'auto-purge, qui se fie à `last_seen`, ne fermait jamais l'orpheline.
79
+ const ecrit = await ecrireSiEncoreVrai(
80
+ `doc_presentations?slug=eq.${enc(slug)}&control_hash=eq.${sha(control)}`,
81
+ { last_seen: new Date().toISOString() },
82
+ );
83
+ // Un heartbeat refusé dit à l'onglet périmé de s'arrêter — c'est une information, pas une panne.
84
+ return ecrit ? { ok: true } : { ok: false, status: 409 };
70
85
  }
71
86
 
72
87
  // Liste des présentations en cours (membre authentifié). Auto-purge : une présentation active dont le
@@ -108,7 +123,12 @@ async function listActivePresentations(email) {
108
123
  const now = Date.now();
109
124
  const stale = list.filter((r) => now - new Date(r.last_seen || r.updated_at || 0).getTime() > STALE_MS).map((r) => r.slug);
110
125
  if (stale.length) {
111
- await PLAYER.db.request(`doc_presentations?slug=in.(${stale.map((s) => enc(s)).join(",")})`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { active: false, updated_at: new Date().toISOString() } }).catch(() => {});
126
+ // ⚠️ L'INACTIVITÉ DANS LA CONDITION, pas seulement dans le calcul. Entre la lecture de la liste
127
+ // et cette écriture, une présentation « orpheline » a pu envoyer son heartbeat : la purger
128
+ // quand même désactivait une session redevenue vivante, sous les pieds de son présentateur.
129
+ // `lte` sur le seuil : seule une ligne dont last_seen n'a PAS bougé est purgée.
130
+ const seuil = new Date(now - STALE_MS).toISOString();
131
+ await PLAYER.db.request(`doc_presentations?slug=in.(${stale.map((s) => enc(s)).join(",")})&active=eq.true&last_seen=lte.${enc(seuil)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { active: false, updated_at: new Date().toISOString() } }).catch(() => {});
112
132
  }
113
133
  const live = list.filter((r) => !stale.includes(r.slug));
114
134
  return live.map((r) => ({ slug: r.slug, docId: r.doc_id, fileUrl: r.file_url, fileName: r.file_name, docTitle: r.doc_title, presenterName: r.presenter_name, ownerName: r.owner_name, ownerAvatar: r.owner_avatar, currentPage: r.current_page || 1, mine: !!(me && r.owner_email && r.owner_email === me), updatedAt: r.updated_at }));
@@ -148,8 +168,18 @@ async function endPresentationByOwner(slug, email, isAdmin) {
148
168
  const row = await getPresentation(slug);
149
169
  if (!row) return { ok: false, status: 404 };
150
170
  if (!isAdmin && (!row.owner_email || row.owner_email !== lc(email))) return { ok: false, status: 403 };
151
- await PLAYER.db.request(`doc_presentations?slug=eq.${enc(slug)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { ...CLOTURE, updated_at: new Date().toISOString() } });
152
- return { ok: true };
171
+ // ⚠️ LE PROPRIÉTAIRE DANS LA CONDITION (l'admin, lui, ferme ce qu'il veut : modération). Une
172
+ // clôture retardée émise par l'ANCIEN propriétaire après un transfert fermait la session du
173
+ // nouveau — le contraire de ce que le transfert venait d'accorder.
174
+ if (isAdmin) {
175
+ await PLAYER.db.request(`doc_presentations?slug=eq.${enc(slug)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { ...CLOTURE, updated_at: new Date().toISOString() } });
176
+ return { ok: true };
177
+ }
178
+ const ecrit = await ecrireSiEncoreVrai(
179
+ `doc_presentations?slug=eq.${enc(slug)}&owner_email=eq.${enc(row.owner_email)}`,
180
+ { ...CLOTURE, updated_at: new Date().toISOString() },
181
+ );
182
+ return ecrit ? { ok: true } : { ok: false, status: 409 };
153
183
  }
154
184
 
155
185
  // Transfert de contrôle : le propriétaire actuel (JWT) désigne un nouveau membre propriétaire (par email).
@@ -161,8 +191,14 @@ async function handoverPresentation(slug, currentEmail, newOwner) {
161
191
  const row = await getPresentation(slug);
162
192
  if (!row) return { ok: false, status: 404 };
163
193
  if (!row.owner_email || row.owner_email !== lc(currentEmail)) return { ok: false, status: 403 };
164
- await PLAYER.db.request(`doc_presentations?slug=eq.${enc(slug)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { owner_user_id: o.id ? String(o.id) : null, owner_email: lc(o.email), owner_name: (o.name || "").slice(0, 120) || null, owner_avatar: (o.avatar || "").slice(0, 600) || null, updated_at: new Date().toISOString() } });
165
- return { ok: true, slug };
194
+ // ⚠️ Deux transferts concurrents depuis le même propriétaire : le premier écrit gagne, le second
195
+ // ne trouve plus sa ligne — sans condition, il ÉCRASAIT le premier et le document changeait de
196
+ // mains deux fois, dans un ordre que personne n'avait choisi.
197
+ const ecrit = await ecrireSiEncoreVrai(
198
+ `doc_presentations?slug=eq.${enc(slug)}&owner_email=eq.${enc(lc(currentEmail))}`,
199
+ { owner_user_id: o.id ? String(o.id) : null, owner_email: lc(o.email), owner_name: (o.name || "").slice(0, 120) || null, owner_avatar: (o.avatar || "").slice(0, 600) || null, updated_at: new Date().toISOString() },
200
+ );
201
+ return ecrit ? { ok: true, slug } : { ok: false, status: 409 };
166
202
  }
167
203
 
168
204
  async function getPresentation(slug) {
@@ -350,6 +386,21 @@ const CHAMPS_PUBLICS = [
350
386
  */
351
387
  const refAuteur = (hash) => (hash ? sha("ref:" + hash).slice(0, 16) : null);
352
388
 
389
+ /**
390
+ * ⚠️ L'IDENTITÉ D'UN RÉACTEUR SE DÉRIVE D'UN SECRET, ELLE NE SE DÉCLARE PAS.
391
+ *
392
+ * Le client envoyait `reactor: MOIREF` — or MOIREF est PUBLIC : chaque participant reçoit les refs
393
+ * de tous les autres dans le tableau des réactions (il en a besoin pour dessiner « j'ai réagi »).
394
+ * N'importe qui pouvait donc copier le ref d'un autre et poser ou retirer SES réactions. Constat
395
+ * du troisième audit, vérifié : les refs transitent en clair dans `reactionsPubliques`.
396
+ *
397
+ * Le jeton d'auteur, lui, ne quitte jamais le navigateur qui l'a tiré. La dérivation est la même
398
+ * que côté client (`referenceAuteur` dans src/live.ts) : sha("ref:" + sha(jeton)), tronqué à 16 —
399
+ * si les deux divergent, « mes » réactions cessent de s'afficher comme miennes, et l'essai qui
400
+ * confronte les deux moteurs le dira.
401
+ */
402
+ const reacteurDepuisJeton = (jeton) => (jeton ? refAuteur(sha(String(jeton))) : null);
403
+
353
404
  /**
354
405
  * ⚠️ LES ANCIENNES RÉACTIONS PORTENT DES ADRESSES, et aucune migration ne les réécrira : le
355
406
  * client stockait `email || nom` comme identité de réacteur. On garde le COMPTE — la pastille
@@ -504,8 +555,13 @@ async function setChatLock(slug, control, locked) {
504
555
  const pres = await getPresentation(slug);
505
556
  if (!pres) return { ok: false, status: 404 };
506
557
  if (!tokenMatches(control, pres.control_hash)) return { ok: false, status: 403 };
507
- await PLAYER.db.request(`doc_presentations?slug=eq.${enc(String(slug || ""))}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { chat_locked: !!locked } });
508
- return { ok: true };
558
+ // Le jeton dans la condition : un verrou posé APRÈS une clôture ou une reprise ailleurs ne
559
+ // trouve plus sa ligne — il ne s'applique pas à une session qui n'est plus la sienne.
560
+ const ecrit = await ecrireSiEncoreVrai(
561
+ `doc_presentations?slug=eq.${enc(String(slug || ""))}&control_hash=eq.${sha(control)}`,
562
+ { chat_locked: !!locked },
563
+ );
564
+ return ecrit ? { ok: true } : { ok: false, status: 409 };
509
565
  }
510
566
 
511
567
  // Réaction emoji (toggle) : le participant (identifié par email ou nom) ajoute/retire un emoji sur un message.
@@ -532,7 +588,10 @@ async function toggleReaction(slug, msgId, emoji, reactor, etat) {
532
588
  // participants comme une réaction, ce qui est absurde même sans être dangereux. Deux barrières,
533
589
  // parce que la seconde protège le jour où quelqu'un lève le plafond de longueur.
534
590
  if (/[A-Za-z_$]/.test(e)) return { ok: false, status: 400 };
535
- const rows = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&slug=eq.${enc(String(slug || ""))}&select=reactions&limit=1`);
591
+ // ⚠️ LE RANG N'EST DEMANDÉ QUE LÀ OÙ IL EXISTE — sélectionner une colonne absente fait échouer
592
+ // la LECTURE entière chez un hôte non migré, et ce serait les réactions qu'on casserait.
593
+ const ordonne = await require("./schema").attendue("reactionsOrdonnees");
594
+ const rows = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&slug=eq.${enc(String(slug || ""))}&select=${ordonne ? "reactions,reactions_seq" : "reactions"}&limit=1`);
536
595
  // ⚠️ LE NOM DE PROPRIÉTÉ ÉCRIT ICI VIENT DU CLIENT — et la garde posée en 0.1.2 ne couvrait que
537
596
  // les LECTURES. `Object.hasOwn` empêchait de LIRE `constructor` ; rien n'empêchait de l'ÉCRIRE.
538
597
  //
@@ -547,16 +606,35 @@ async function toggleReaction(slug, msgId, emoji, reactor, etat) {
547
606
  //
548
607
  // Un objet SANS prototype retire la classe entière : il n'y a plus rien à masquer ni à
549
608
  // atteindre, quelle que soit la clé et quel que soit le plafond.
550
- const brut = (Array.isArray(rows) && rows[0] && rows[0].reactions && typeof rows[0].reactions === "object" && !Array.isArray(rows[0].reactions)) ? rows[0].reactions : {};
551
- const cur = Object.assign(Object.create(null), brut);
552
- const arr = Array.isArray(cur[e]) ? cur[e] : [];
553
- const i = arr.indexOf(who);
554
- const veut = etat === undefined || etat === null ? (i < 0) : !!etat;
555
- if (veut && i < 0) arr.push(who);
556
- if (!veut && i >= 0) arr.splice(i, 1);
557
- if (arr.length) cur[e] = arr; else delete cur[e];
558
- const maj = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&select=*`, { method: "PATCH", headers: { Prefer: "return=representation" }, body: { reactions: cur } });
559
- return { ok: true, message: premierPublic(maj) };
609
+ // ⚠️ LIRE-MODIFIER-RÉÉCRIRE PERD DES ÉCRITURES : deux participants qui réagissent dans la même
610
+ // seconde lisent le même JSON, et la seconde réécriture emporte la première — une réaction
611
+ // disparaît, sans erreur nulle part. Même remède que le pilotage (0002) : l'écriture porte le
612
+ // rang qu'elle a LU, la base ne l'accepte que s'il n'a pas bougé, sinon on relit et on rejoue.
613
+ // Trois rejeux suffisent : chaque tour, au moins UN écrivain gagne — l'attente est bornée par le
614
+ // nombre de concurrents réels, pas par la malchance.
615
+ let lu = rows;
616
+ for (let essai = 0; essai < 4; essai += 1) {
617
+ const brut = (Array.isArray(lu) && lu[0] && lu[0].reactions && typeof lu[0].reactions === "object" && !Array.isArray(lu[0].reactions)) ? lu[0].reactions : {};
618
+ const rang = ordonne ? Math.trunc(Number((Array.isArray(lu) && lu[0] && lu[0].reactions_seq) || 0)) : 0;
619
+ // Objet NU déclaré tel quel : la garde statique des écritures indexées reconnaît la
620
+ // déclaration, pas un voisinage — et c'est elle qui a refusé la première forme de cette boucle.
621
+ const cur = Object.create(null);
622
+ Object.assign(cur, brut);
623
+ const arr = Array.isArray(cur[e]) ? cur[e] : [];
624
+ const i = arr.indexOf(who);
625
+ const veut = etat === undefined || etat === null ? (i < 0) : !!etat;
626
+ if (veut && i < 0) arr.push(who);
627
+ if (!veut && i >= 0) arr.splice(i, 1);
628
+ if (arr.length) cur[e] = arr; else delete cur[e];
629
+ const condition = ordonne ? `&reactions_seq=eq.${rang}` : "";
630
+ const maj = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}${condition}&select=*`, { method: "PATCH", headers: { Prefer: "return=representation" }, body: { reactions: cur, ...(ordonne ? { reactions_seq: rang + 1 } : {}) } });
631
+ if (!ordonne || (Array.isArray(maj) && maj.length)) return { ok: true, message: premierPublic(maj) };
632
+ // Rang dépassé : quelqu'un a écrit entre notre lecture et notre écriture. On relit, on rejoue.
633
+ lu = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&slug=eq.${enc(String(slug || ""))}&select=reactions,reactions_seq&limit=1`);
634
+ }
635
+ // Quatre tours perdus : plus de concurrents que de raisons d'attendre. Un refus net vaut mieux
636
+ // qu'une écriture qui écraserait ce que les gagnants viennent de poser.
637
+ return { ok: false, status: 409 };
560
638
  }
561
639
 
562
640
  // ── Statistiques de présentation (assistance) ────────────────────────────────────────────────────────────
@@ -767,4 +845,5 @@ async function listPresentationsForDoc(docId, email, isAdmin, autoriseLarge) {
767
845
  }
768
846
 
769
847
  module.exports = {
848
+ reacteurDepuisJeton,
770
849
  messagePublic, CHAMPS_PUBLICS, init, createPresentation, getPresentation, setPage, endPresentation, addMessage, listMessages, toggleReaction, editMessage, deleteMessage, setChatLock, createUploadUrl, reclaimPresentation, touchPresentation, listActivePresentations, handoverPresentation, endPresentationByOwner, recordAttendance, presentationStats, listPresentationsForDoc, switchPresentationDoc, setPresentationContent };
package/server/schema.js CHANGED
@@ -42,6 +42,11 @@ const ATTENDUES = {
42
42
  migration: "supabase/migrations/0005-envoi-unique.sql",
43
43
  fonction: "empêcher qu'un renvoi crée un second message",
44
44
  },
45
+ reactionsOrdonnees: {
46
+ table: "doc_presentation_messages", colonne: "reactions_seq",
47
+ migration: "supabase/migrations/0006-reactions-ordonnees.sql",
48
+ fonction: "empêcher deux réactions simultanées de s'écraser",
49
+ },
45
50
  };
46
51
 
47
52
  /**
@@ -71,6 +76,25 @@ let PLAYER = null;
71
76
  */
72
77
  const connues = new Map();
73
78
 
79
+ /**
80
+ * ⚠️ UN « NON » N'A PAS LA MÊME DURÉE DE VIE QU'UN « OUI » — et les confondre a fait qu'une panne
81
+ * passagère ÉTEIGNAIT des fonctions pour la vie du processus.
82
+ *
83
+ * Un « oui » est stable : une colonne présente ne disparaît pas (les migrations sont additives, et
84
+ * personne ne supprime une colonne sous un processus qui tourne). Un « non », lui, recouvre deux
85
+ * réalités que la sonde ne distingue pas — colonne absente, base injoignable — dont la seconde se
86
+ * répare TOUTE SEULE. Retenir ce « non » pour toujours transformait un hoquet de base pendant un
87
+ * envoi de message en idempotence morte jusqu'au redémarrage, sans un mot.
88
+ *
89
+ * D'où : un « oui » est retenu pour le processus ; un « non » expire. Le délai borne le coût chez
90
+ * un hôte réellement non migré (une sonde ratée par minute et par attente, pas une par écriture)
91
+ * tout en rendant la guérison automatique — la seule direction où l'erreur se répare toute seule.
92
+ * Trouvé par le troisième audit, qui a aussi montré que notre essai de récupération appelait
93
+ * `init()` — lequel vide précisément ce cache : il prouvait une guérison qui n'existait pas.
94
+ */
95
+ const TTL_NEGATIF_MS = 60 * 1000;
96
+ const quandNegatif = new Map(); // cle → instant du « non », pour le faire expirer
97
+
74
98
  /**
75
99
  * Les réponses DÉJÀ OBTENUES, pour qui veut les lire — la carte d'identité, essentiellement.
76
100
  *
@@ -84,6 +108,9 @@ function init(ctx) {
84
108
  PLAYER = ctx;
85
109
  connues.clear();
86
110
  reponses.clear();
111
+ quandNegatif.clear();
112
+ sondeEnCours = null;
113
+ sondePrise = 0;
87
114
  }
88
115
 
89
116
  /** La sonde d'une attente déclarée. C'est la seule forme d'appel que les appelants utilisent. */
@@ -146,6 +173,12 @@ function etatDuSchema() {
146
173
  */
147
174
  function aLaColonne(table, colonne, migration) {
148
175
  const cle = `${table}.${colonne}`;
176
+ // Un « non » périmé n'est plus une réponse : on repose la question. Un « oui » ne périme pas.
177
+ const depuis = quandNegatif.get(cle);
178
+ if (depuis !== undefined && Date.now() - depuis > TTL_NEGATIF_MS) {
179
+ connues.delete(cle);
180
+ quandNegatif.delete(cle);
181
+ }
149
182
  if (!connues.has(cle)) connues.set(cle, sonder(table, colonne, migration, cle));
150
183
  return connues.get(cle);
151
184
  }
@@ -166,6 +199,7 @@ async function sonder(table, colonne, migration, cle) {
166
199
  // portage a un fichier à réécrire, pas une habitude à retrouver partout.
167
200
  const champ = encodeURIComponent(colonne);
168
201
  await PLAYER.db.request(`${table}?select=${champ}&limit=0`);
202
+ quandNegatif.delete(cle);
169
203
  noter(cle, true, migration);
170
204
  return true;
171
205
  } catch {
@@ -174,6 +208,7 @@ async function sonder(table, colonne, migration, cle) {
174
208
  // message d'erreur, c'est-à-dire de dépendre du texte d'un service tiers. Ce qui change entre
175
209
  // les deux, c'est la durée : une base injoignable le redevient, et le processus suivant reposera
176
210
  // la question.
211
+ quandNegatif.set(cle, Date.now());
177
212
  noter(cle, false, migration);
178
213
  signaler(cle, migration);
179
214
  return false;
@@ -201,7 +236,24 @@ function verdict(manque, sondees, attendues) {
201
236
  * messages jusqu'au prochain démarrage. Une route de contrôle qui casse la production. Si le
202
237
  * témoin ne répond pas, on ne sonde RIEN et on ne retient RIEN.
203
238
  */
204
- async function sonderTout() {
239
+ /**
240
+ * ⚠️ LA SONDE EST PUBLIQUE : chaque appel coûtait des requêtes base, autant de fois qu'on veut.
241
+ * Une boucle sur `?contract=1&schema=1` faisait de la carte un petit amplificateur — la ressource
242
+ * PARTAGÉE paie, pas l'appelant. Deux bornes : les appels simultanés partagent UNE sonde, et le
243
+ * résultat sert pendant 30 s — un état de schéma ne change pas plus vite qu'une migration.
244
+ */
245
+ const CACHE_SONDE_MS = 30 * 1000;
246
+ let sondeEnCours = null;
247
+ let sondePrise = 0;
248
+
249
+ function sonderTout() {
250
+ if (sondeEnCours && Date.now() - sondePrise < CACHE_SONDE_MS) return sondeEnCours;
251
+ sondePrise = Date.now();
252
+ sondeEnCours = vraimentSonderTout();
253
+ return sondeEnCours;
254
+ }
255
+
256
+ async function vraimentSonderTout() {
205
257
  // ⚠️ LA PART ENCODÉE EST CALCULÉE À PART, comme dans `sonder()` dix lignes plus haut, et pour la
206
258
  // même raison : la garde de portabilité traque une parenthèse après « select= », et un appel de
207
259
  // fonction écrit dans le gabarit en produit une. J'ai reproduit ici le défaut dont le correctif
@@ -215,6 +267,11 @@ async function sonderTout() {
215
267
  // dit que cette mesure-ci n'a pas eu lieu. Taire l'un ou l'autre serait mentir d'un côté.
216
268
  return { ...etatDuSchema(), verdict: "indetermine" };
217
269
  }
270
+ // ⚠️ LE TÉMOIN VIENT DE RÉPONDRE : tout « non » encore en cache est SUSPECT — il peut dater
271
+ // d'une panne guérie. On le jette et on repose la question, sinon ce diagnostic rendrait la
272
+ // valeur d'un incident passé en la datant d'aujourd'hui. Les « oui » restent : ils sont stables.
273
+ for (const [cle] of quandNegatif) { connues.delete(cle); reponses.delete(cle); }
274
+ quandNegatif.clear();
218
275
  for (const nom of Object.keys(ATTENDUES)) await attendue(nom);
219
276
  return etatDuSchema();
220
277
  }
@@ -238,6 +295,6 @@ function signaler(cle, migration) {
238
295
  }
239
296
 
240
297
  /** Pour les tests et l'exploitation : reposer la question. */
241
- function oublier() { connues.clear(); reponses.clear(); }
298
+ function oublier() { connues.clear(); reponses.clear(); quandNegatif.clear(); sondeEnCours = null; sondePrise = 0; }
242
299
 
243
300
  module.exports = { init, aLaColonne, attendue, etatDuSchema, sonderTout, oublier, ATTENDUES, TEMOIN };
package/supabase/init.sql CHANGED
@@ -126,7 +126,10 @@ create index if not exists commercial_doc_internal_sessions_last_at_idx
126
126
  -- que le droit de suivre.
127
127
  create table if not exists public.doc_presentations (
128
128
  slug text primary key,
129
- control_hash text not null,
129
+ -- ⚠️ NULLABLE, ET C'EST LE CŒUR DU MODÈLE : null = archivée (plus de pilote). Déclarée NOT NULL
130
+ -- jusqu'à la 0008, ce qui rendait la CLÔTURE impossible sur une base neuve — trouvé par le banc
131
+ -- vraie base, seul endroit où une contrainte peut refuser quelque chose.
132
+ control_hash text,
130
133
  doc_id text,
131
134
  file_url text not null,
132
135
  file_name text,
@@ -173,7 +176,10 @@ create table if not exists public.doc_presentation_messages (
173
176
  attachment jsonb,
174
177
  -- Clé d'idempotence fabriquée par le client AVANT le premier envoi et réutilisée au renvoi :
175
178
  -- un renvoi réseau ne crée pas un second message. Nulle si le client ne la fournit pas.
176
- client_key text
179
+ client_key text,
180
+ -- Rang de la dernière écriture de réactions acceptée : deux réactions simultanées ne
181
+ -- s'écrasent plus — l'écriture conditionnée à un rang dépassé ne modifie rien, le serveur rejoue.
182
+ reactions_seq bigint not null default 0
177
183
  );
178
184
  create index if not exists dpm_slug_idx on public.doc_presentation_messages (slug, created_at);
179
185
  -- ⚠️ Portée sur (slug, client_key), pas sur la clé seule : deux présentations n'ont aucune raison
@@ -310,15 +316,18 @@ alter table public.doc_bot_sessions enable row level security;
310
316
  -- le présentateur sans qu'aucune table ne soit lisible publiquement. C'est ce qui permet de ne
311
317
  -- créer aucune politique de lecture au-dessus.
312
318
  --
313
- -- ⚠️ LE CHAT N'EST PAS ENCORE SUR CE CHEMIN. Ses messages sont livrés par `postgres_changes`, qui
314
- -- exige un SELECT au niveau de la TABLE — donc ouvert à quiconque détient la clé publiable, et
315
- -- pour TOUTES les présentations, pas seulement la sienne. Ce fichier refuse ce compromis : la
316
- -- table est publiée, mais AUCUNE politique de lecture n'est créée.
319
+ -- ⚠️ LE CHAT AUSSI EST SUR CE CHEMIN, DÉSORMAIS. Un message émet un signal broadcast (`msg`,
320
+ -- `msg-upd`) et chaque participant RELIT l'historique par la route du player — le signal dit
321
+ -- « quelque chose a changé », jamais le contenu. Aucune politique de lecture n'est donc
322
+ -- nécessaire, et le chat est en direct sur une installation neuve.
317
323
  --
318
- -- Conséquence assumée sur une installation neuve : un participant voit l'historique du chat en
319
- -- arrivant, et les messages suivants à l'actualisation — pas en direct. Le chat n'est pas cassé,
320
- -- il est différé. Le portage du chat en broadcast lèvera cette limite ; d'ici là, mieux vaut une
321
- -- fonctionnalité partiellement en retard qu'une base où chacun lit les conversations des autres.
324
+ -- (Ce commentaire a affirmé le contraire pendant plusieurs versions après le portage du chat en
325
+ -- broadcast — le troisième audit l'a relevé. Un commentaire périmé dans un fichier d'installation
326
+ -- n'est pas un détail : il pousse un mainteneur à « réparer » un différé qui n'existe plus, en
327
+ -- ajoutant précisément la politique de lecture que ce fichier refuse.)
328
+ --
329
+ -- La publication de la table reste utile aux INSTANCES HISTORIQUES encore sur `postgres_changes`,
330
+ -- et inoffensive ici : publiée sans politique de lecture, elle ne livre rien à personne.
322
331
  --
323
332
  -- ⚠️ Ne mettez de toute façon jamais de donnée confidentielle dans un message de chat ni dans un
324
333
  -- nom d'auteur.
@@ -336,6 +345,43 @@ begin
336
345
  end $$;
337
346
  alter table public.doc_presentation_messages replica identity full;
338
347
 
348
+ -- ── Le scellé de l'archive ─────────────────────────────────────────────────────────────────────
349
+ -- Sept chemins d'écriture vérifient l'archive PUIS écrivent — dans une AUTRE table que celle qui
350
+ -- porte l'état. Aucun filtre PostgREST ne peut fermer cette fenêtre : l'arbitre est la base.
351
+ -- `FOR KEY SHARE` bloque une clôture concurrente jusqu'au commit de l'écriture — c'est lui qui
352
+ -- rend le refus atomique, pas le simple test.
353
+ create or replace function public.player_archive_scellee()
354
+ returns trigger
355
+ language plpgsql
356
+ as $$
357
+ declare
358
+ v_active boolean;
359
+ v_control text;
360
+ begin
361
+ select p.active, p.control_hash into v_active, v_control
362
+ from public.doc_presentations p
363
+ where p.slug = new.slug
364
+ for key share;
365
+ -- Pas de présentation : rien à sceller (chat d'un lien sans session live, données historiques).
366
+ if not found then return new; end if;
367
+ if v_active = false and v_control is null then
368
+ raise exception 'presentation archivee : ecriture refusee'
369
+ using errcode = 'P0001', hint = 'l''archive est en lecture seule';
370
+ end if;
371
+ return new;
372
+ end
373
+ $$;
374
+
375
+ drop trigger if exists dpm_archive_scellee on public.doc_presentation_messages;
376
+ create trigger dpm_archive_scellee
377
+ before insert or update on public.doc_presentation_messages
378
+ for each row execute function public.player_archive_scellee();
379
+
380
+ drop trigger if exists dpa_archive_scellee on public.doc_presentation_attendees;
381
+ create trigger dpa_archive_scellee
382
+ before insert or update on public.doc_presentation_attendees
383
+ for each row execute function public.player_archive_scellee();
384
+
339
385
  -- ── Rattrapage : bases installées depuis un init.sql plus ancien ───────────────────────────────
340
386
  --
341
387
  -- ⚠️ CE FICHIER A ÉTÉ INCOMPLET, ET RIEN NE LE DISAIT. Il annonçait « un seul fichier, sans rien à
@@ -355,3 +401,8 @@ alter table public.doc_presentations
355
401
  add column if not exists write_seq bigint not null default 0;
356
402
  alter table public.doc_presentation_messages
357
403
  add column if not exists client_key text;
404
+ -- Une base née d'un init.sql d'avant la 0008 porte un NOT NULL qui rend la clôture impossible.
405
+ alter table public.doc_presentations
406
+ alter column control_hash drop not null;
407
+ alter table public.doc_presentation_messages
408
+ add column if not exists reactions_seq bigint not null default 0;
@@ -0,0 +1,17 @@
1
+ -- DEUX RÉACTIONS SIMULTANÉES S'ÉCRASAIENT.
2
+ --
3
+ -- Le serveur lit le JSON des réactions, le modifie en mémoire, le réécrit en entier : deux
4
+ -- participants qui réagissent au même message dans la même seconde lisent le même état, et la
5
+ -- seconde écriture emporte la première — une réaction disparaît, sans erreur nulle part.
6
+ --
7
+ -- Même remède que pour les écritures de pilotage (0002) : un rang. L'écriture porte le rang
8
+ -- qu'elle a lu et n'est acceptée que s'il n'a pas bougé ; sinon le serveur relit et rejoue.
9
+ -- La comparaison et l'écriture deviennent le même geste — c'est la base qui arbitre, pas la
10
+ -- mémoire d'un processus.
11
+ alter table public.doc_presentation_messages
12
+ add column if not exists reactions_seq bigint not null default 0;
13
+
14
+ comment on column public.doc_presentation_messages.reactions_seq is
15
+ 'Rang de la dernière écriture de réactions acceptée. Une écriture conditionnée à un rang déjà '
16
+ 'dépassé ne modifie aucune ligne : le serveur relit et rejoue. Sans lui : rien ne casse, mais '
17
+ 'deux réactions simultanées peuvent s''écraser (dernier écrivain gagne), comme avant la 0006.';
@@ -0,0 +1,49 @@
1
+ -- L'ARCHIVE ÉTAIT VÉRIFIÉE, PUIS L'ÉCRITURE PARTAIT SANS ELLE.
2
+ --
3
+ -- Sept chemins d'écriture (message, édition, suppression, réaction, présence, verrou, pièce
4
+ -- jointe) demandent d'abord si la présentation est archivée, puis écrivent. Entre les deux, une
5
+ -- clôture peut passer : un message atterrit dans une archive « en lecture seule ». La fenêtre est
6
+ -- étroite, mais la promesse — une archive ne bouge plus — était tenue par du code, pas par la base.
7
+ --
8
+ -- ⚠️ AUCUN FILTRE POSTGREST NE PEUT FERMER ÇA : les écritures visent doc_presentation_messages et
9
+ -- doc_presentation_attendees, la condition vit dans doc_presentations — une AUTRE table. Sans
10
+ -- transaction ni RPC côté player, l'arbitre ne peut être que la base elle-même. D'où ce trigger.
11
+ --
12
+ -- ⚠️ `FOR KEY SHARE` EST CE QUI REND LE REFUS ATOMIQUE, pas le simple test. Sans lui, une clôture
13
+ -- peut se COMMITTER entre la vérification du trigger et le commit de l'écriture — la fenêtre
14
+ -- rétrécit mais survit. Le verrou de ligne bloque la clôture concurrente jusqu'au commit de
15
+ -- l'écriture : l'ordre devient réel, plus probable.
16
+ --
17
+ -- Sans lui : rien ne casse — les sept chemins gardent leur vérification préalable, et la fenêtre
18
+ -- de course reste ce qu'elle était avant cette migration. Le trigger est le scellé, pas la porte.
19
+ create or replace function public.player_archive_scellee()
20
+ returns trigger
21
+ language plpgsql
22
+ as $$
23
+ declare
24
+ v_active boolean;
25
+ v_control text;
26
+ begin
27
+ select p.active, p.control_hash into v_active, v_control
28
+ from public.doc_presentations p
29
+ where p.slug = new.slug
30
+ for key share;
31
+ -- Pas de présentation : rien à sceller (chat d'un lien sans session live, données historiques).
32
+ if not found then return new; end if;
33
+ if v_active = false and v_control is null then
34
+ raise exception 'presentation archivee : ecriture refusee'
35
+ using errcode = 'P0001', hint = 'l''archive est en lecture seule';
36
+ end if;
37
+ return new;
38
+ end
39
+ $$;
40
+
41
+ drop trigger if exists dpm_archive_scellee on public.doc_presentation_messages;
42
+ create trigger dpm_archive_scellee
43
+ before insert or update on public.doc_presentation_messages
44
+ for each row execute function public.player_archive_scellee();
45
+
46
+ drop trigger if exists dpa_archive_scellee on public.doc_presentation_attendees;
47
+ create trigger dpa_archive_scellee
48
+ before insert or update on public.doc_presentation_attendees
49
+ for each row execute function public.player_archive_scellee();
@@ -0,0 +1,19 @@
1
+ -- CLÔTURER ÉTAIT IMPOSSIBLE SUR UNE BASE NEUVE.
2
+ --
3
+ -- La clôture d'une présentation pose `control_hash = null` : c'est LE marqueur d'archive — le slug
4
+ -- seul donne le droit de suivre, le hash celui de piloter, et une archive n'a plus de pilote.
5
+ -- Or init.sql déclarait la colonne NOT NULL : chaque clôture violait la contrainte (23502) et
6
+ -- toute présentation restait « active » pour toujours chez un hôte installé depuis ce fichier.
7
+ --
8
+ -- ⚠️ TROUVÉ PAR LE BANC « VRAIE BASE », PAS PAR UN LECTEUR : 793 essais passaient — le double en
9
+ -- mémoire n'a pas de contraintes, c'est écrit dans son en-tête. Les bases HISTORIQUES, nées de
10
+ -- migrations plus anciennes, sont nullables : la production ne pouvait pas le montrer non plus.
11
+ --
12
+ -- Sans lui : sur une base née d'un init.sql d'avant cette correction, `endPresentation` échoue en
13
+ -- erreur serveur et l'archive en lecture seule n'existe pas — les présentations ne se ferment pas.
14
+ alter table public.doc_presentations
15
+ alter column control_hash drop not null;
16
+
17
+ comment on column public.doc_presentations.control_hash is
18
+ 'Hash du jeton de pilotage. NULL = présentation archivée : plus personne ne pilote, et le '
19
+ 'scellé (0007) refuse toute écriture. Le slug seul ne donne que le droit de suivre.';