discovery-media-player 0.1.63 → 0.1.65

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/README.md CHANGED
@@ -70,7 +70,7 @@ This player answers the same questions and runs on your own infrastructure.
70
70
  | | What you get |
71
71
  |---|---|
72
72
  | **Tracked links** | One link per recipient. Revocable. Re-shares are chained to their parent, so you see when a document travels. |
73
- | **Reading analytics** | Time per page, furthest page reached, device. Counted only while the tab is visible and focusedan open tab in the background is not reading. |
73
+ | **Reading analytics** | Time per page, furthest page reached, device. Counted only while the tab is visible and recently active a hidden tab, or one idle past the threshold, is not reading. |
74
74
  | **Live presentation** | Present a document to a remote audience, with chat, presence, and handover. The audience follows your page without a video call. |
75
75
  | **Access wall** | Optional: a document can require an email + code before it opens. |
76
76
  | **Brand per client** | The loader carries your client's logo, resolved at display time — fix a logo and links already in inboxes follow. |
@@ -29,6 +29,7 @@ need.
29
29
  "capabilities": ["docshare", "presentations", "embed-denied", "host-fetch", "brand-reference", "host-auth", "host-share", "host-mail"],
30
30
  "frameAncestors": ["'self'", "https://*.vercel.app", "https://app.example.com"],
31
31
  "separateIssuer": true,
32
+ "internalStrict": true,
32
33
  "hostShare": true,
33
34
  "hostMail": true,
34
35
  "plugins": { "bot": false, "visitors": false, "brandIntro": false, "botBrowser": false, "providerQuotas": false },
@@ -89,6 +90,13 @@ exactly the monitoring case where "no data" would otherwise read as "all clear".
89
90
  ⚠️ **`incomplet` wins over `partiel`**: a missing column is a positive fact and settles the verdict
90
91
  on its own, even when the rest has not been checked.
91
92
 
93
+ ⚠️ **`internalStrict: false` means internal identity comes from the browser.** In transitional
94
+ mode the internal-analytics route accepts `docId`, `email` and `name` as the client declares them —
95
+ a caller can fabricate "this colleague read this document". Strict mode (`PLAYER_INTERNAL_STRICT=1`)
96
+ only accepts host-signed tokens. The flag exists so monitoring can **refuse** a non-strict instance
97
+ instead of discovering it in a log; treat `false` as an alert on any instance whose host already
98
+ issues internal tokens. The default will flip to strict at the next announced breaking change.
99
+
92
100
  ⚠️ **`couvre` states the scope, because `complet` without a scope overpromises.** The card checks
93
101
  the **conditional columns** — the ones a feature probes before writing. The rate-limit migrations
94
102
  (`0003`, `0004`) are deliberately **not** in it: a host may provide its own `limits` capability, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
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: "0ba592364fc3eb81" };
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: "23c99a1e3e559f8e" };
package/server/handler.js CHANGED
@@ -93,7 +93,13 @@ const LEAFLET = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js";
93
93
  // scripts, donc l'intégrité restera hors de portée ici ; épingler la version trimestrielle rend
94
94
  // au moins le changement DÉCIDÉ, et daté. À relever à chaque montée — Google retire les versions
95
95
  // après environ un an.
96
- const MAPS_VERSION = "3.58";
96
+ // ⚠️ UNE VERSION INVALIDE N'EST PAS UNE ERREUR, C'EST UN CANAL PAR DÉFAUT SILENCIEUX. Google ne
97
+ // sert qu'une fenêtre glissante de versions (~4 trimestres) ; en dehors, le paramètre est IGNORÉ
98
+ // et le canal hebdomadaire se charge — l'épinglage devient une illusion sans qu'aucune erreur ne
99
+ // le dise. « 3.58 » a menti ainsi pendant des mois (cinquième audit). À vérifier à chaque
100
+ // trimestre — le commentaire d'à côté doit porter la date du dernier contrôle.
101
+ // Contrôlé le 18/08/2026 : fenêtre servie 3.62 → 3.65.
102
+ const MAPS_VERSION = "3.65";
97
103
 
98
104
  /**
99
105
  * Empreintes des dépendances tierces (constat P2-4).
@@ -336,9 +342,10 @@ var Live=(function(){
336
342
  // qui exige que cette table soit lisible publiquement — donc, avec la clé publiable, les
337
343
  // conversations de TOUTES les présentations, pas seulement la sienne. C'était le dernier
338
344
  // obstacle avant de pouvoir fermer cette lecture.
339
- // L'émetteur diffuse la ligne que le serveur vient d'écrire ; les autres l'ajoutent chez eux.
345
+ // L'émetteur SIGNALE qu'un message existe ; chacun émetteur compris tient son affichage de
346
+ // la RÉPONSE du serveur ou de la relecture HTTP, jamais du canal.
340
347
  // ⚠️ La diffusion ne revient pas à son émetteur : il ajoute donc SA copie lui-même — depuis la
341
- // RÉPONSE du serveur, jamais depuis le canal.
348
+ // RÉPONSE du serveur.
342
349
  //
343
350
  // ⚠️ LA CHARGE EST VIDE, ET C'EST CE QUI REND LA PROPRIÉTÉ STRUCTURELLE. Elle transportait la
344
351
  // ligne projetée — que le récepteur IGNORAIT (il relit par HTTP, cf. le commentaire du
@@ -347,13 +354,18 @@ var Live=(function(){
347
354
  // était plus forte que le code, et tenait lieu de garde sans en être une — le jour où un
348
355
  // récepteur nouveau aurait lu la charge « puisqu'elle est là », la projection serait devenue
349
356
  // optionnelle sans que rien ne le dise. Le second hôte a levé l'écart ; sendState et
350
- // sendMap avaient déjà la bonne forme. Un commentaire ici même affirmait l'ancien monde
351
- // (« les autres l'ajoutent chez eux ») : il a fait dériver sa lecture, pas la nôtre — un
352
- // commentaire n'est pas du code, et il vieillit sans essai pour le contredire.
357
+ // sendMap avaient déjà la bonne forme. Le commentaire d'en tête de ce bloc a affirmé l'ancien
358
+ // monde (« les autres l'ajoutent chez eux ») pendant des versions : il a fait dériver la
359
+ // lecture du second hôte puis a SURVÉCU à sa propre citation : le correctif qui le désignait
360
+ // comme menteur l'a cité au lieu de le réécrire, et documenter un défaut l'avait rendu
361
+ // intouchable. Réécrit à la relecture suivante, par le même hôte. Un commentaire n'est pas du
362
+ // code : il vieillit sans essai pour le contredire — et un essai fige désormais la charge vide.
353
363
  function sendMsg(m){if(!m)return;try{if(ch)ch.send({type:'broadcast',event:'msg',payload:{}});}catch(e){}}
354
364
  function sendMsgUpd(m){if(!m)return;try{if(ch)ch.send({type:'broadcast',event:'msg-upd',payload:{}});}catch(e){}}
355
365
  // Édition, suppression, réaction : le serveur renvoie la ligne à jour, on l'applique chez soi
356
- // puis on la diffuse. Même chemin pour les trois — une seule façon de se tromper.
366
+ // puis on SIGNALE — la charge part vide, les autres relisent. Même chemin pour les trois — une
367
+ // seule façon de se tromper. (« Puis on la diffuse » a survécu ici une version de trop : second
368
+ // exemplaire du commentaire d'en tête, trouvé par le second hôte dans le tarball publié.)
357
369
  function majDiffusee(r){return r.json().then(function(d){if(d&&d.ok&&d.message){updateMsg(d.message);sendMsgUpd(d.message);}}).catch(function(){});}
358
370
  function onState(fn){_onState=fn;}
359
371
  // Badge « non lus » sur le bouton chat/FAB (panneau fermé) + pulse du FAB. Aperçu (ticker) au nouveau message.
@@ -3576,24 +3588,43 @@ async function handler(req, res) {
3576
3588
  const dejaLa = await PLAYER.db.request(
3577
3589
  `commercial_doc_shares?doc_id=eq.${encodeURIComponent(docId)}&created_by=is.null&recipient_email=is.null${filtreAtteste}&select=slug&limit=1`,
3578
3590
  );
3591
+ const cleHote = `hote:${docId}|${atteste || ""}`;
3579
3592
  if (Array.isArray(dejaLa) && dejaLa[0]) {
3593
+ // Réemploi d'une ligne historique (sans clé) : on lui POSE la clé au passage — les
3594
+ // demandes suivantes la trouveront par l'unicité, et les doublons d'avant 0011
3595
+ // s'éteignent d'eux-mêmes faute d'être resservis.
3596
+ const cleDispo = await require("./schema").attendue("liensUniques");
3580
3597
  await PLAYER.db.request(`commercial_doc_shares?slug=eq.${encodeURIComponent(dejaLa[0].slug)}`, {
3581
3598
  method: "PATCH", headers: { Prefer: "return=minimal" },
3582
- body: { doc_title: body.docTitle || null, file_url: String(body.fileUrl), file_name: body.fileName || null, revoked: false },
3599
+ body: { doc_title: body.docTitle || null, file_url: String(body.fileUrl), file_name: body.fileName || null, revoked: false, ...(cleDispo ? { idem_key: cleHote } : {}) },
3583
3600
  });
3584
3601
  return jd(200, { ok: true, slug: dejaLa[0].slug, reused: true });
3585
3602
  }
3586
3603
  // `createdBy` reste NUL : personne ne l'a créé. Le filtre « mes liens » compare
3587
3604
  // `created_by=eq.<email>`, qui exclut les NUL — ce lien n'apparaît donc dans la liste
3588
3605
  // de personne, et reste visible en administration (`list.all`, qui ne filtre pas).
3589
- const neuf = await createShare({
3590
- brandKey: body.brandKey, docId, docTitle: body.docTitle, fileUrl: body.fileUrl,
3591
- fileName: body.fileName, createdBy: null, attestedRecipientEmail: atteste || null,
3592
- bot: body.bot, botScript: body.botScript,
3593
- guided: body.guided, profileId: body.profileId, allowDownload: body.allowDownload,
3594
- videoLayout: body.videoLayout, logo: body.logo, logoDark: body.logoDark,
3595
- });
3596
- return jd(200, { ok: true, slug: neuf.slug, reused: false });
3606
+ // ⚠️ LA LECTURE-PUIS-ÉCRITURE NE PROMET RIEN : deux demandes dans la même seconde
3607
+ // passaient toutes deux le « déjà là ? » et DEUX liens naissaient — les statistiques du
3608
+ // document se fragmentent entre eux. La contrainte (0011) promet ; le 409 qu'elle rend
3609
+ // est une CONFIRMATION : l'autre demande a gagné, on relit son lien et on le rend
3610
+ // `reused` même règle que le renvoi d'un message (0005).
3611
+ try {
3612
+ const neuf = await createShare({
3613
+ brandKey: body.brandKey, docId, docTitle: body.docTitle, fileUrl: body.fileUrl,
3614
+ fileName: body.fileName, createdBy: null, attestedRecipientEmail: atteste || null,
3615
+ bot: body.bot, botScript: body.botScript,
3616
+ guided: body.guided, profileId: body.profileId, allowDownload: body.allowDownload,
3617
+ videoLayout: body.videoLayout, logo: body.logo, logoDark: body.logoDark,
3618
+ idemKey: cleHote,
3619
+ });
3620
+ return jd(200, { ok: true, slug: neuf.slug, reused: false });
3621
+ } catch (erreur) {
3622
+ if (!String((erreur && erreur.message) || "").includes("409")) throw erreur;
3623
+ try { PLAYER.errors.capture(new Error("lien hôte déjà créé par une demande simultanée : " + docId), { route: "hostshare", benin: true }); } catch { /* jamais bloquant */ }
3624
+ const gagnant = await PLAYER.db.request(`commercial_doc_shares?idem_key=eq.${encodeURIComponent(cleHote)}&select=slug&limit=1`);
3625
+ if (!Array.isArray(gagnant) || !gagnant[0]) throw erreur; // 409 d'autre chose : on ne l'invente pas
3626
+ return jd(200, { ok: true, slug: gagnant[0].slug, reused: true });
3627
+ }
3597
3628
  }
3598
3629
 
3599
3630
  const u = await PLAYER.identity.verifyToken(req.headers.authorization);
@@ -3631,12 +3662,23 @@ async function handler(req, res) {
3631
3662
  const docId = String(body.docId || "");
3632
3663
  if (!docId || !body.fileUrl) return jd(400, { ok: false, error: "docId/fileUrl requis" });
3633
3664
  const ex = await PLAYER.db.request(`commercial_doc_shares?doc_id=eq.${encodeURIComponent(docId)}&is_test=eq.true&select=slug&limit=1`);
3665
+ const cleTest = `repetition:${docId}`;
3634
3666
  if (Array.isArray(ex) && ex[0]) {
3635
- await PLAYER.db.request(`commercial_doc_shares?slug=eq.${encodeURIComponent(ex[0].slug)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { doc_title: body.docTitle || null, file_url: String(body.fileUrl), file_name: body.fileName || null, bot_enabled: true, bot_guided: true, bot_profile_id: (body.profileId || "").trim() || null, revoked: false } });
3667
+ const cleDispo = await require("./schema").attendue("liensUniques");
3668
+ await PLAYER.db.request(`commercial_doc_shares?slug=eq.${encodeURIComponent(ex[0].slug)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { doc_title: body.docTitle || null, file_url: String(body.fileUrl), file_name: body.fileName || null, bot_enabled: true, bot_guided: true, bot_profile_id: (body.profileId || "").trim() || null, revoked: false, ...(cleDispo ? { idem_key: cleTest } : {}) } });
3636
3669
  return jd(200, { ok: true, slug: ex[0].slug });
3637
3670
  }
3638
- const t = await createShare({ docId, docTitle: body.docTitle, fileUrl: body.fileUrl, fileName: body.fileName, recipientName: "Répétition (test)", createdBy: u.email, bot: true, guided: true, profileId: body.profileId, isTest: true });
3639
- return jd(200, { ok: true, slug: t.slug });
3671
+ // Deux ouvertures simultanées de la répétition : la contrainte tranche, le perdant relit.
3672
+ try {
3673
+ const t = await createShare({ docId, docTitle: body.docTitle, fileUrl: body.fileUrl, fileName: body.fileName, recipientName: "Répétition (test)", createdBy: u.email, bot: true, guided: true, profileId: body.profileId, isTest: true, idemKey: cleTest });
3674
+ return jd(200, { ok: true, slug: t.slug });
3675
+ } catch (erreur) {
3676
+ if (!String((erreur && erreur.message) || "").includes("409")) throw erreur;
3677
+ try { PLAYER.errors.capture(new Error("lien de répétition déjà créé par une demande simultanée : " + docId), { route: "docshare-test", benin: true }); } catch { /* jamais bloquant */ }
3678
+ const gagnant = await PLAYER.db.request(`commercial_doc_shares?idem_key=eq.${encodeURIComponent(cleTest)}&select=slug&limit=1`);
3679
+ if (!Array.isArray(gagnant) || !gagnant[0]) throw erreur;
3680
+ return jd(200, { ok: true, slug: gagnant[0].slug, reused: true });
3681
+ }
3640
3682
  }
3641
3683
  if (body.action === "docshare.revoke") {
3642
3684
  await revokeShare(String(body.slug || ""));
@@ -3908,6 +3950,12 @@ async function handler(req, res) {
3908
3950
  // l'hôte connaît déjà son émetteur — il veut seulement savoir si l'instance le regarde.
3909
3951
  // Dire lequel n'aiderait personne et renseignerait qui sonde.
3910
3952
  separateIssuer: !!(PLAYER.config && PLAYER.config.separateIssuer),
3953
+ // ⚠️ « L'identité interne vient-elle d'un JETON, ou du navigateur ? » En mode transitoire
3954
+ // (false), la route interne accepte docId/email/name tels que le client les déclare — un
3955
+ // appelant peut fabriquer « tel collègue a consulté tel document ». Le booléen rend l'état
3956
+ // MESURABLE : un cockpit peut refuser une instance non stricte, au lieu de le découvrir en
3957
+ // lisant un journal. Demandé par le second hôte (cinquième audit, P1-4).
3958
+ internalStrict: !!(PLAYER.config && PLAYER.config.internalStrict),
3911
3959
  // ⚠️ L'ÉTAT DU SCHÉMA, LÀ OÙ ON REGARDE. Une colonne absente était signalée par un
3912
3960
  // `console.warn`, une fois par processus : sur une fonction serverless, une ligne perdue
3913
3961
  // dans une sortie que personne n'ouvre tant que tout a l'air de marcher — et « tout a
@@ -446,6 +446,11 @@ function messagePublic(row) {
446
446
  // relit sans avoir à vérifier d'où vient chaque clé.
447
447
  const out = Object.create(null);
448
448
  for (const c of CHAMPS_PUBLICS) if (c in row) out[c] = row[c];
449
+ // ⚠️ LA CEINTURE : une ligne supprimée sort VIDE, quoi que porte encore la base. Les courses
450
+ // d'avant ce correctif — et les lignes historiques qu'elles ont laissées — peuvent avoir gardé
451
+ // un corps, une pièce jointe ou une citation sous deleted=true : la projection est le dernier
452
+ // endroit qui peut refuser de les resservir, et le seul qui couvre AUSSI le passé.
453
+ if (out.deleted) { out.body = ""; out.attachment = null; out.reactions = {}; out.reply_text = null; out.reply_name = null; }
449
454
  if ("reactions" in out) out.reactions = reactionsPubliques(out.reactions);
450
455
  // Dérivé, jamais recopié : `author_hash` est lu pour ça et ne sort jamais tel quel.
451
456
  if ("author_hash" in row) out.author_ref = refAuteur(row.author_hash);
@@ -551,7 +556,13 @@ async function editMessage(slug, msgId, authorToken, body) {
551
556
  const rows = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&slug=eq.${enc(String(slug || ""))}&select=author_hash,deleted&limit=1`);
552
557
  const m = Array.isArray(rows) && rows[0]; if (!m || m.deleted) return { ok: false, status: 404 };
553
558
  if (!m.author_hash || m.author_hash !== sha(authorToken)) return { ok: false, status: 403 };
554
- const maj = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&select=*`, { method: "PATCH", headers: { Prefer: "return=representation" }, body: { body: b, edited: true } });
559
+ // ⚠️ CE QUI A ÉTÉ VÉRIFIÉ VOYAGE DANS LA CONDITION. Une suppression entre la lecture et
560
+ // l'écriture vidait le message — et l'édition retardée RÉÉCRIVAIT le texte dans une ligne
561
+ // marquée supprimée : effacé à l'écran, ressuscité dans le JSON. Cinquième audit, P1-3.
562
+ // `deleted=eq.false` fait de « pas supprimé » une propriété de l'ÉCRITURE ; `author_hash` et
563
+ // `slug` par cohérence — l'écriture ne repose plus sur rien qu'elle n'exige elle-même.
564
+ const maj = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&slug=eq.${enc(String(slug || ""))}&author_hash=eq.${sha(authorToken)}&deleted=eq.false&select=*`, { method: "PATCH", headers: { Prefer: "return=representation" }, body: { body: b, edited: true } });
565
+ if (!Array.isArray(maj) || !maj.length) return { ok: false, status: 409 };
555
566
  return { ok: true, message: premierPublic(maj) };
556
567
  }
557
568
 
@@ -566,7 +577,16 @@ async function deleteMessage(slug, msgId, { authorToken, control }) {
566
577
  let byPresenter = false;
567
578
  if (!byAuthor && control) { const pres = await getPresentation(slug); byPresenter = !!(pres && tokenMatches(control, pres.control_hash)); }
568
579
  if (!byAuthor && !byPresenter) return { ok: false, status: 403 };
569
- const maj = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&select=*`, { method: "PATCH", headers: { Prefer: "return=representation" }, body: { deleted: true, body: "", reactions: {}, attachment: null } });
580
+ // ⚠️ Par l'AUTEUR : son hash dans la condition l'écriture n'exige que ce qu'elle a vérifié.
581
+ // Par le PRÉSENTATEUR : le jeton vit dans une AUTRE table, aucun filtre PostgREST ne peut le
582
+ // porter — la fenêtre résiduelle (un ex-présentateur modère dans la seconde où on lui reprend
583
+ // la main) est assumée : elle ne rend qu'un droit qu'il avait légitimement l'instant d'avant.
584
+ // `deleted=eq.false` partout : supprimer deux fois est IDEMPOTENT — zéro ligne veut dire
585
+ // « déjà supprimé », et c'est l'état que l'appelant voulait. `reply_text`/`reply_name` partent
586
+ // aussi : un message supprimé ne doit pas continuer de citer ce qu'il citait.
587
+ const cond = byAuthor ? `&author_hash=eq.${sha(authorToken)}` : "";
588
+ const maj = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&slug=eq.${enc(String(slug || ""))}${cond}&deleted=eq.false&select=*`, { method: "PATCH", headers: { Prefer: "return=representation" }, body: { deleted: true, body: "", reactions: {}, attachment: null, reply_text: null, reply_name: null } });
589
+ if (!Array.isArray(maj) || !maj.length) return { ok: true, deja: true };
570
590
  return { ok: true, message: premierPublic(maj) };
571
591
  }
572
592
 
@@ -612,7 +632,7 @@ async function toggleReaction(slug, msgId, emoji, reactor, etat) {
612
632
  // ⚠️ LE RANG N'EST DEMANDÉ QUE LÀ OÙ IL EXISTE — sélectionner une colonne absente fait échouer
613
633
  // la LECTURE entière chez un hôte non migré, et ce serait les réactions qu'on casserait.
614
634
  const ordonne = await require("./schema").attendue("reactionsOrdonnees");
615
- 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`);
635
+ const rows = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&slug=eq.${enc(String(slug || ""))}&select=${ordonne ? "reactions,reactions_seq,deleted" : "reactions,deleted"}&limit=1`);
616
636
  // ⚠️ LE NOM DE PROPRIÉTÉ ÉCRIT ICI VIENT DU CLIENT — et la garde posée en 0.1.2 ne couvrait que
617
637
  // les LECTURES. `Object.hasOwn` empêchait de LIRE `constructor` ; rien n'empêchait de l'ÉCRIRE.
618
638
  //
@@ -635,6 +655,10 @@ async function toggleReaction(slug, msgId, emoji, reactor, etat) {
635
655
  // nombre de concurrents réels, pas par la malchance.
636
656
  let lu = rows;
637
657
  for (let essai = 0; essai < 4; essai += 1) {
658
+ // ⚠️ Un message supprimé PENDANT la boucle : on s'arrête — réagir à un message effacé le
659
+ // ferait revivre dans le tableau des réactions. Et `deleted=eq.false` dans la condition
660
+ // d'écriture ferme la course entre cette lecture et le PATCH.
661
+ if (Array.isArray(lu) && lu[0] && lu[0].deleted) return { ok: false, status: 404 };
638
662
  const brut = (Array.isArray(lu) && lu[0] && lu[0].reactions && typeof lu[0].reactions === "object" && !Array.isArray(lu[0].reactions)) ? lu[0].reactions : {};
639
663
  const rang = ordonne ? Math.trunc(Number((Array.isArray(lu) && lu[0] && lu[0].reactions_seq) || 0)) : 0;
640
664
  // Objet NU déclaré tel quel : la garde statique des écritures indexées reconnaît la
@@ -647,11 +671,14 @@ async function toggleReaction(slug, msgId, emoji, reactor, etat) {
647
671
  if (veut && i < 0) arr.push(who);
648
672
  if (!veut && i >= 0) arr.splice(i, 1);
649
673
  if (arr.length) cur[e] = arr; else delete cur[e];
650
- const condition = ordonne ? `&reactions_seq=eq.${rang}` : "";
674
+ const condition = (ordonne ? `&reactions_seq=eq.${rang}` : "") + "&deleted=eq.false";
651
675
  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 } : {}) } });
652
- if (!ordonne || (Array.isArray(maj) && maj.length)) return { ok: true, message: premierPublic(maj) };
676
+ if (Array.isArray(maj) && maj.length) return { ok: true, message: premierPublic(maj) };
677
+ // Hôte non migré (pas de rang) : zéro ligne ne peut venir que de `deleted` — le message est
678
+ // parti pendant qu'on écrivait. Pas de rejeu qui le ferait revivre.
679
+ if (!ordonne) return { ok: false, status: 404 };
653
680
  // Rang dépassé : quelqu'un a écrit entre notre lecture et notre écriture. On relit, on rejoue.
654
- lu = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&slug=eq.${enc(String(slug || ""))}&select=reactions,reactions_seq&limit=1`);
681
+ lu = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&slug=eq.${enc(String(slug || ""))}&select=reactions,reactions_seq,deleted&limit=1`);
655
682
  }
656
683
  // Quatre tours perdus : plus de concurrents que de raisons d'attendre. Un refus net vaut mieux
657
684
  // qu'une écriture qui écraserait ce que les gagnants viennent de poser.
@@ -794,7 +821,12 @@ async function switchPresentationDoc(slug, email, isAdmin, { fileUrl, fileName,
794
821
  if (!row) return { ok: false, status: 404 };
795
822
  if (estClose(row)) return { ok: false, status: 409, error: "ended" };
796
823
  if (!isAdmin && (!row.owner_email || row.owner_email !== lc(email))) return { ok: false, status: 403 };
797
- const ecrit = await ecrireSiEncoreVrai(`doc_presentations?slug=eq.${enc(slug)}&active=eq.true`, {
824
+ // ⚠️ LE PROPRIÉTAIRE VÉRIFIÉ VOYAGE DANS LA CONDITION — un transfert entre la lecture et
825
+ // l'écriture donnait la présentation à Bob, et la requête retardée d'Alice changeait ENCORE le
826
+ // document de Bob. Même classe que reprise/clôture/transfert (#130), deux survivants trouvés par
827
+ // le cinquième audit. L'admin, lui, écrit sur la présentation active telle quelle : modération.
828
+ const ecrit = await ecrireSiEncoreVrai(
829
+ `doc_presentations?slug=eq.${enc(slug)}&active=eq.true${isAdmin ? "" : `&owner_email=eq.${enc(row.owner_email)}`}`, {
798
830
  file_url: String(fileUrl), file_name: fileName || null, doc_title: docTitle || null, doc_id: docId ? String(docId) : null,
799
831
  content: null, current_page: 1, active: true, last_seen: new Date().toISOString(), updated_at: new Date().toISOString(),
800
832
  });
@@ -859,10 +891,12 @@ async function setPresentationContent(slug, email, isAdmin, content, control) {
859
891
  // droit en fermant une porte. Le PILOTE écrit tant que son jeton vaut : une carte affichée
860
892
  // pendant que le portable dormait doit encore partir, comme une page. Le PROPRIÉTAIRE, lui,
861
893
  // n'écrit que sur une présentation vivante : c'est déjà ce que le contrôle au-dessus exige.
894
+ // ⚠️ Et le PROPRIÉTAIRE (non-admin) voyage dans la condition, comme partout : sans ça, sa
895
+ // requête retardée écrivait encore sur la présentation qu'un transfert venait de donner.
862
896
  const ecrit = await ecrireSiEncoreVrai(
863
897
  pilote
864
898
  ? `doc_presentations?slug=eq.${enc(slug)}&control_hash=eq.${sha(control)}`
865
- : `doc_presentations?slug=eq.${enc(slug)}&active=eq.true`,
899
+ : `doc_presentations?slug=eq.${enc(slug)}&active=eq.true${isAdmin ? "" : `&owner_email=eq.${enc(row.owner_email)}`}`,
866
900
  { content: sanitizeContent(content), active: true, last_seen: new Date().toISOString(), updated_at: new Date().toISOString() },
867
901
  );
868
902
  if (!ecrit) return { ok: false, status: 409, error: "ended" };
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
+ liensUniques: {
46
+ table: "commercial_doc_shares", colonne: "idem_key",
47
+ migration: "supabase/migrations/0011-liens-uniques.sql",
48
+ fonction: "empêcher deux demandes simultanées de créer deux liens système pour le même usage",
49
+ },
45
50
  reactionsOrdonnees: {
46
51
  table: "doc_presentation_messages", colonne: "reactions_seq",
47
52
  migration: "supabase/migrations/0006-reactions-ordonnees.sql",
package/server/shares.js CHANGED
@@ -15,10 +15,15 @@ function newSlug() { return crypto.randomBytes(9).toString("base64url"); } // ~1
15
15
 
16
16
  // Crée un lien de partage (un par destinataire). Dénormalise titre/URL/nom pour résilience (le doc vit dans
17
17
  // un snapshot). Renvoie le slug.
18
- async function createShare({ docId, docTitle, fileUrl, fileName, recipientEmail, recipientName, attestedRecipientEmail, createdBy, bot, botScript, guided, profileId, allowDownload, isTest, videoLayout, logo, logoDark, brandKey}) {
18
+ async function createShare({ docId, docTitle, fileUrl, fileName, recipientEmail, recipientName, attestedRecipientEmail, createdBy, bot, botScript, guided, profileId, allowDownload, isTest, videoLayout, logo, logoDark, brandKey, idemKey}) {
19
19
  if (!docId || !fileUrl) throw Object.assign(new Error("doc invalide"), { statusCode: 400 });
20
20
  const slug = newSlug();
21
+ // ⚠️ LA CLÉ N'EST ÉCRITE QUE LÀ OÙ LA COLONNE EXISTE — PostgREST rejette le POST ENTIER sur une
22
+ // colonne inconnue : chez un hôte non migré, ce n'est pas l'unicité qu'on perdrait, c'est la
23
+ // CRÉATION de liens. Même sonde que la clé des messages (0005).
24
+ const cleDispo = idemKey ? await require("./schema").attendue("liensUniques") : false;
21
25
  const row = {
26
+ ...(cleDispo ? { idem_key: String(idemKey).slice(0, 300) } : {}),
22
27
  slug, doc_id: String(docId), doc_title: docTitle || null, file_url: String(fileUrl), file_name: fileName || null,
23
28
  recipient_email: low(recipientEmail) || null, recipient_name: (recipientName || "").trim() || null, created_by: low(createdBy) || null,
24
29
  // ⚠️ DEUX CHAMPS, DEUX FAITS. `recipient_email` dit qui a le droit d'EXPÉDIER en son nom au
package/supabase/init.sql CHANGED
@@ -51,11 +51,17 @@ create table if not exists public.commercial_doc_shares (
51
51
  brand_dark boolean not null default false,
52
52
  require_auth boolean not null default false,
53
53
  brand_key text, -- ⚠️ RÉFÉRENCE résolue à l'affichage (branding.forKey)
54
+ -- Clé d'idempotence des liens SYSTÈME (hôte, répétition) — nulle sur les liens ordinaires.
55
+ -- Unique quand renseignée : deux demandes simultanées ne créent qu'un lien.
56
+ idem_key text,
54
57
  -- Destinataire attesté par l'hôte : sert à ATTRIBUER une lecture, jamais à expédier en son nom.
55
58
  -- `recipient_email`, elle, dit qui peut expédier — vide quand personne ne le peut.
56
59
  attested_recipient_email text
57
60
  );
58
61
  create index if not exists cds_doc_id_idx on public.commercial_doc_shares (doc_id);
62
+ create unique index if not exists cds_idem_key_uniq
63
+ on public.commercial_doc_shares (idem_key)
64
+ where idem_key is not null;
59
65
  create index if not exists cds_parent_idx on public.commercial_doc_shares (parent_slug);
60
66
  create index if not exists idx_doc_shares_brand_key
61
67
  on public.commercial_doc_shares (brand_key) where brand_key is not null;
@@ -269,6 +275,9 @@ begin
269
275
  then now() + make_interval(secs => p_window_seconds)
270
276
  else l.expires_at end
271
277
  returning l.count into v_count;
278
+
279
+ -- Le refus est décidé ICI, sur la valeur qu'on vient d'écrire : le client n'a rien à recalculer,
280
+ -- donc rien à recalculer sur une valeur déjà périmée.
272
281
  return query select (v_count <= p_max), v_count;
273
282
  end;
274
283
  $$;
@@ -326,9 +335,6 @@ alter table public.doc_bot_sessions enable row level security;
326
335
  -- n'est pas un détail : il pousse un mainteneur à « réparer » un différé qui n'existe plus, en
327
336
  -- ajoutant précisément la politique de lecture que ce fichier refuse.)
328
337
  --
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.
331
- --
332
338
  -- ⚠️ Ne mettez de toute façon jamais de donnée confidentielle dans un message de chat ni dans un
333
339
  -- nom d'auteur.
334
340
  -- ⚠️ AUCUNE TABLE DU PLAYER N'EST PUBLIÉE DANS `supabase_realtime`, ET C'EST UN CHOIX. Le direct
@@ -344,8 +350,11 @@ alter table public.doc_bot_sessions enable row level security;
344
350
  -- ── Le scellé de l'archive ─────────────────────────────────────────────────────────────────────
345
351
  -- Sept chemins d'écriture vérifient l'archive PUIS écrivent — dans une AUTRE table que celle qui
346
352
  -- porte l'état. Aucun filtre PostgREST ne peut fermer cette fenêtre : l'arbitre est la base.
347
- -- `FOR KEY SHARE` bloque une clôture concurrente jusqu'au commit de l'écriture c'est lui qui
348
- -- rend le refus atomique, pas le simple test.
353
+ -- ⚠️ `FOR SHARE`, PAS `FOR KEY SHARE` la clôture modifie des colonnes NON-CLÉS, son UPDATE prend
354
+ -- FOR NO KEY UPDATE, que KEY SHARE ne bloque pas. La 0007 a porté ce verrou trop faible pendant
355
+ -- une version, avec un commentaire qui affirmait l'atomicité (corrigé par la 0010, constat du
356
+ -- cinquième audit — VU au banc à deux transactions de la forge : le message entrait dans
357
+ -- l'archive). C'est FOR SHARE qui rend le refus atomique.
349
358
  create or replace function public.player_archive_scellee()
350
359
  returns trigger
351
360
  language plpgsql
@@ -357,7 +366,7 @@ begin
357
366
  select p.active, p.control_hash into v_active, v_control
358
367
  from public.doc_presentations p
359
368
  where p.slug = new.slug
360
- for key share;
369
+ for share;
361
370
  -- Pas de présentation : rien à sceller (chat d'un lien sans session live, données historiques).
362
371
  if not found then return new; end if;
363
372
  if v_active = false and v_control is null then
@@ -397,6 +406,8 @@ alter table public.doc_presentations
397
406
  add column if not exists write_seq bigint not null default 0;
398
407
  alter table public.doc_presentation_messages
399
408
  add column if not exists client_key text;
409
+ alter table public.commercial_doc_shares
410
+ add column if not exists idem_key text;
400
411
  -- Une base née d'un init.sql d'avant la 0008 porte un NOT NULL qui rend la clôture impossible.
401
412
  alter table public.doc_presentations
402
413
  alter column control_hash drop not null;
@@ -9,10 +9,11 @@
9
9
  -- doc_presentation_attendees, la condition vit dans doc_presentations — une AUTRE table. Sans
10
10
  -- transaction ni RPC côté player, l'arbitre ne peut être que la base elle-même. D'où ce trigger.
11
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.
12
+ -- ⚠️ LE VERROU CI-DESSOUS EST TROP FAIBLE, ET LA 0010 LE REMPLACE. Cette migration affirmait que
13
+ -- FOR KEY SHARE rendait le refus atomique : faux la clôture modifie des colonnes non-clés, son
14
+ -- UPDATE prend FOR NO KEY UPDATE, compatible avec KEY SHARE. La fenêtre prétendue fermée restait
15
+ -- ouverte (cinquième audit, VU au banc à deux transactions). Le fichier reste tel qu'appliqué par
16
+ -- les bases historiques ; la correction est une migration de plus, comme toujours.
16
17
  --
17
18
  -- Sans lui : rien ne casse — les sept chemins gardent leur vérification préalable, et la fenêtre
18
19
  -- de course reste ce qu'elle était avant cette migration. Le trigger est le scellé, pas la porte.
@@ -0,0 +1,38 @@
1
+ -- LE SCELLÉ DE L'ARCHIVE PRÉTENDAIT UNE ATOMICITÉ QUE SON VERROU NE FOURNISSAIT PAS.
2
+ --
3
+ -- La 0007 verrouillait la présentation en FOR KEY SHARE — et son commentaire affirmait que c'était
4
+ -- « ce qui rend le refus atomique ». Faux : la clôture modifie `active` et `control_hash`, des
5
+ -- colonnes NON-CLÉS, donc son UPDATE prend FOR NO KEY UPDATE — que KEY SHARE ne bloque PAS
6
+ -- (matrice des verrous PostgreSQL : KEY SHARE ne conflit qu'avec FOR UPDATE). Le trigger vérifiait
7
+ -- « ouverte », la clôture se committait sous lui, le message entrait dans l'archive : la fenêtre
8
+ -- que 0007 prétendait fermer était ouverte. Constat P1-1 du cinquième audit — VU au banc de la
9
+ -- forge : deux transactions psql, l'écriture n'attendait pas la clôture.
10
+ --
11
+ -- FOR SHARE, lui, conflit avec FOR NO KEY UPDATE : une clôture non committée fait ATTENDRE
12
+ -- l'écriture (qui voit ensuite l'archive et est refusée) ; une écriture en cours fait attendre la
13
+ -- clôture. Le coût est nul en pratique : plusieurs écritures partagent le verrou entre elles,
14
+ -- seule la clôture entre en conflit — précisément l'objectif.
15
+ --
16
+ -- Sans lui : le scellé garde sa valeur de refus (une écriture APRÈS la clôture committée est
17
+ -- toujours refusée) — seule la fenêtre concurrente reste ouverte, comme entre 0007 et 0010.
18
+ create or replace function public.player_archive_scellee()
19
+ returns trigger
20
+ language plpgsql
21
+ as $$
22
+ declare
23
+ v_active boolean;
24
+ v_control text;
25
+ begin
26
+ select p.active, p.control_hash into v_active, v_control
27
+ from public.doc_presentations p
28
+ where p.slug = new.slug
29
+ for share;
30
+ -- Pas de présentation : rien à sceller (chat d'un lien sans session live, données historiques).
31
+ if not found then return new; end if;
32
+ if v_active = false and v_control is null then
33
+ raise exception 'presentation archivee : ecriture refusee'
34
+ using errcode = 'P0001', hint = 'l''archive est en lecture seule';
35
+ end if;
36
+ return new;
37
+ end
38
+ $$;
@@ -0,0 +1,27 @@
1
+ -- DEUX DEMANDES SIMULTANÉES CRÉAIENT DEUX LIENS POUR LE MÊME USAGE.
2
+ --
3
+ -- Le lien « de l'hôte » (un par document et destinataire attesté) et le lien de répétition (un par
4
+ -- document) sont censés être UNIQUES : le code lisait « existe-t-il déjà ? » puis insérait — deux
5
+ -- requêtes dans la même seconde passaient toutes deux la lecture, et deux liens naissaient. Les
6
+ -- statistiques du document se fragmentent entre eux, sans que rien ne le dise. Constat P2 du
7
+ -- cinquième audit, même famille que l'idempotence des messages (0005) : la lecture-puis-écriture
8
+ -- ne promet rien, seule une contrainte promet.
9
+ --
10
+ -- ⚠️ La clé est NULLE sur les liens ordinaires (un membre peut envoyer le même document à autant
11
+ -- de destinataires qu'il veut) — l'index partiel ne contraint que les liens SYSTÈME, qui la
12
+ -- portent. Les doublons historiques restent en base, clé nulle : leurs URL sont distribuées, on
13
+ -- ne retire rien — le premier trouvé est réutilisé et reçoit la clé, les autres s'éteignent
14
+ -- d'eux-mêmes faute d'être resservis.
15
+ --
16
+ -- Sans lui : rien ne casse — la lecture-puis-écriture continue, avec sa course, comme avant.
17
+ alter table public.commercial_doc_shares
18
+ add column if not exists idem_key text;
19
+
20
+ comment on column public.commercial_doc_shares.idem_key is
21
+ 'Clé d''idempotence des liens SYSTÈME (hôte, répétition) : « hote:<doc>|<destinataire attesté> » '
22
+ 'ou « repetition:<doc> ». Nulle sur les liens ordinaires. Unique quand renseignée : deux '
23
+ 'demandes simultanées ne créent qu''un lien, la seconde relit le gagnant.';
24
+
25
+ create unique index if not exists cds_idem_key_uniq
26
+ on public.commercial_doc_shares (idem_key)
27
+ where idem_key is not null;