discovery-media-player 0.1.64 → 0.1.66

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 focused — an 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. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.64",
3
+ "version": "0.1.66",
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",
@@ -77,5 +77,8 @@
77
77
  "typescript": "^5.5.4",
78
78
  "typescript-eslint": "^8.46.4",
79
79
  "vitest": "^4.1.10"
80
+ },
81
+ "dependencies": {
82
+ "pdfjs-dist": "6.2.108"
80
83
  }
81
84
  }
@@ -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,Ue=Object.defineProperties,De=Object.getOwnPropertyDescriptor,We=Object.getOwnPropertyDescriptors,Ve=Object.getOwnPropertyNames,he=Object.getOwnPropertySymbols;var Me=Object.prototype.hasOwnProperty,ze=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={}))Me.call(n,t)&&ve(e,t,n[t]);if(he)for(var t of he(n))ze.call(n,t)&&ve(e,t,n[t]);return e},we=(e,n)=>Ue(e,We(n));var v=(e,n)=>{for(var t in n)C(e,t,{get:n[t],enumerable:!0})},Fe=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of Ve(n))!Me.call(e,o)&&o!==t&&C(e,o,{get:()=>n[o],enumerable:!(r=De(n,o))||r.enumerable});return e};var Be=e=>Fe(C({},\"__esModule\",{value:!0}),e);var Bn={};v(Bn,{bridge:()=>N,cadence:()=>L,chat:()=>H,live:()=>$,presentation:()=>U,presentationState:()=>W,tracking:()=>O,viewer:()=>D});var N={};v(N,{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 i=e.slug;return typeof i!=\"string\"||!je.test(i)?null:{type:o,slug:i}}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=i=>{if(!Xe(t,i.source))return;let a=Te(i.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function en(e,n){var i;let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=(i=t.parent)!=null?i:null,o=a=>{if(!r||r===t||a.source!==r)return;let l=_e(a.data);l&&e(l)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var O={};v(O,{createTracker:()=>ln});var L={};v(L,{PRESENTER_ACTIONS_PER_HOUR:()=>X,PRESENT_CACHE_MS:()=>rn,PRESENT_QUOTA_MARGIN:()=>Ae,PRESENT_QUOTA_PER_HOUR:()=>on,PRESENT_READS_PER_HOUR:()=>Re,PRESENT_READ_BURST:()=>Pe,PRESENT_READ_COALESCE_MS:()=>Ie,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,Ie=400,rn=Ie,Ae=4,on=(Re+Pe)*q*Ae;var sn=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function an(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(16));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}return Math.random().toString(36).slice(2)+Date.now().toString(36)}function un(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;o&&typeof o.sendBeacon==\"function\"?o.sendBeacon(e,new Blob([r],{type:\"application/json\"})):n.fetch(e,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:r,keepalive:!0})}catch(r){}}}function ln(e={}){var ge,be;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",i=(ge=e.idleMs)!=null?ge:Q,a=(be=e.sessionEveryMs)!=null?be:12e3,l=e.slug||\"\",d=e.internal||null,m=e.sessionId||an(),p=e.send||un(o,n),c={},u=0,g=0,f=0,M=0,h=null,E=r(),S=!1,V=!1,I=[],He=()=>!!l||!!d,ce=s=>{if(He())try{p(s)}catch(b){}},z=()=>!!t&&t.visibilityState===\"visible\"&&!S,de=s=>{let b=r()-s,x=Math.max(0,E-s)+i;return Math.max(0,Math.min(b,x))},F=()=>{h!=null&&u>0&&(c[u]=(c[u]||0)+de(h)/1e3),h=z()?r():null},pe=()=>{F(),h=null},B=()=>{h==null&&z()&&(h=r())},j=()=>{let s=0;for(let b in c)s+=c[b];return h!=null&&(s+=de(h)/1e3),Math.round(s)},fe=()=>{let s={};for(let b in c)s[b]=Math.round(c[b]);return s},k=()=>{E=r(),S&&(S=!1,B())},me=(s,b)=>{d||ce({slug:l,event:s,page:b||g,maxPage:f,seconds:j(),sessionId:m})},A=()=>{F();let s={event:\"session\",sessionId:m,numPages:M,maxPage:f,totalSeconds:j(),pagesTime:fe()};d?(s.internal=!0,s.docId=d.docId,s.email=d.email,s.name=d.name,d.it&&(s.it=d.it)):s.slug=l,ce(s)},R=(s,b,x,ye)=>{!s||typeof s.addEventListener!=\"function\"||(s.addEventListener(b,x,ye),I.push(()=>{s.removeEventListener(b,x,ye)}))};return{sessionId:m,totalSeconds:j,maxPage:()=>f,pageTimes:fe,start(){if(V)return;V=!0,me(\"open\",1),u=1,E=r(),h=z()?r():null,R(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(pe(),A()):(k(),B())}),R(n,\"focus\",()=>{k(),B()});for(let x of sn)R(n,x,k,{passive:!0});R(e.scrollElement,\"scroll\",k,{passive:!0}),R(n,\"beforeunload\",A);let s=n.setInterval(()=>{!S&&r()-E>i&&(S=!0,pe())},5e3),b=n.setInterval(A,a);I.push(()=>{n.clearInterval(s),n.clearInterval(b)})},setPage(s){!s||s===g||(E=r(),F(),u=s,g=s,s>f&&(f=s,me(\"page\",s)))},setPageCount(s){M=s||0},noteActivity:k,flush:A,stop(){for(;I.length;){let s=I.pop();try{s==null||s()}catch(b){}}V=!1}}}var $={};v($,{STORAGE_KEYS:()=>w,attendeeKey:()=>fn,authorToken:()=>mn,avatarHtml:()=>ee,canModerate:()=>oe,createBudget:()=>wn,createFileEcritures:()=>xn,createScheduler:()=>vn,escapeHtml:()=>y,fetchBorne:()=>Mn,flattenPresence:()=>cn,formatMessageBody:()=>ne,initials:()=>Ce,isMentioned:()=>bn,isMine:()=>T,presenceId:()=>Tn,reactorId:()=>re,referenceAuteur:()=>gn,shouldNotify:()=>yn,unreadLabel:()=>hn});var w={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(w.attendeeKey);if(r)return r;let o=`anon-${t()}`;return e.setItem(w.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,w.authorToken);if(t)return t;let r=n();return pn(e,w.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 i=await t.digest(\"SHA-256\",new TextEncoder().encode(o));return Array.from(new Uint8Array(i),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,M)=>setTimeout(f,M)),i=n.clearTimer||(f=>clearTimeout(f)),a=!1,l=!1,d=-1/0,m=null,p=!1;function c(){if(p||a){l=!0;return}let f=t-(r()-d);if(f>0){u(f);return}a=!0,l=!1,d=r();let M=!1;e(()=>{M||(M=!0,a=!1,l&&!p&&c())})}function u(f){p||m!==null||(m=o(()=>{m=null,c()},Math.max(0,f)))}return{signaler(){p||c()},maintenant(){p||(d=-1/0,c())},arreter(){p=!0,m!==null&&(i(m),m=null)}}}function Mn(e,n,t=J,r={}){let o=r.fetch||(typeof fetch==\"function\"?fetch:void 0);if(!o)return Promise.reject(new Error(\"fetch indisponible\"));let i=r.setTimer||((u,g)=>setTimeout(u,g)),a=r.clearTimer||(u=>clearTimeout(u)),l=null;try{l=new AbortController}catch(u){}let d=l?we(G({},n||{}),{signal:l.signal}):G({},n||{}),m=null,p=new Promise((u,g)=>{m=i(()=>{try{l==null||l.abort()}catch(f){}g(new Error(\"delai\"))},t)}),c=()=>{try{a(m)}catch(u){}};return Promise.race([o(e,d),p]).then(u=>(c(),u),u=>{throw c(),u})}function wn(e){let n=e.now||(()=>Date.now()),t=Math.max(1,e.rafale),r=t,o=n();return{prendre(){let i=n();return r=Math.min(t,r+(i-o)*e.parHeure/36e5),o=i,r<1?!1:(r-=1,!0)},restants:()=>r}}function xn(e={}){var m;let n=(m=e.minMs)!=null?m:0,t=e.now||(()=>Date.now()),r=e.setTimer||((p,c)=>setTimeout(p,c)),o=new Map,i=!1,a=-1/0,l=!1;function d(){if(i||o.size===0)return;let p=n-(t()-a);if(p>0){if(l)return;l=!0,r(()=>{l=!1,d()},p);return}let c=o.keys().next().value,u=o.get(c);o.delete(c),i=!0,a=t(),Promise.resolve().then(u.tache).then(g=>u.regler(g),()=>u.regler(null)).then(()=>{i=!1,d()})}return{poser(p,c){return new Promise(u=>{let g=o.get(p);o.set(p,{tache:c,regler:f=>{g&&g.regler(f),u(f)}}),d()})},enAttente:()=>o.size}}function Tn(e,n=te){try{if(e){let t=e.getItem(w.presenceId);if(t)return t;let r=`p-${n()}`;return e.setItem(w.presenceId,r),r}}catch(t){}return`p-${n()}`}var H={};v(H,{messageClassName:()=>En,renderActions:()=>Oe,renderAttachment:()=>Le,renderMessage:()=>_n,renderReactions:()=>Ne});function Ne(e,n){let t=e&&e.reactions||{},r=re(n),o=\"\";for(let i in t){let a=t[i]||[];if(!a.length)continue;let l=a.indexOf(r)>=0;o+=`<button class=\"re-chip${l?\" on\":\"\"}\" data-e=\"${y(i)}\">${y(i)} ${a.length}</button>`}return o}function Le(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>`:\"\",i=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${ee(e.author_avatar,e.author_name)}</span><span class=b><div class=who><b>${y(e.author_name||\"Invit\\xE9\")}</b>${i}</div>`+t+o+Le(e.attachment)+`<div class=cm-re>${Ne(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 U={};v(U,{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:()=>Ln,DEFAULT_ASPECT:()=>An,MAX_ZOOM:()=>In,MIN_PAGE_WIDTH:()=>Cn,MIN_ZOOM:()=>Pn,PRERENDER_MARGIN:()=>Nn,arrowState:()=>Hn,averageColor:()=>Wn,clampPage:()=>$e,clampZoom:()=>On,fitWidth:()=>Dn,isImageDocument:()=>Vn,progressPercent:()=>$n});var Pn=.5,In=3,An=1.35,Cn=280,Nn=\"500px 0px\",Ln=\"-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 $e(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function $n(e,n){return n?Math.round($e(e,n)/n*100):0}function Hn(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var Un=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,i=((e.containerHeight||600)-Un-(e.overlap||0)-(e.reserve||0))/r;return i>120?Math.min(t,i):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 i=0;i<e.length;i+=4)t+=e[i],r+=e[i+1],o+=e[i+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function Vn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var W={};v(W,{presentationTransition:()=>zn,switchDocUrl:()=>Fn});function zn(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=le(e.content);if(t)return[{kind:\"show-map\",content:t}];let r=[{kind:\"leave-map\"}];return e.file_url&&e.file_url!==n.docUrl?(r.push({kind:\"switch-doc\",url:e.file_url,name:e.file_name||void 0,title:e.doc_title||void 0,updatedAt:e.updated_at||void 0}),r):(e.current_page&&r.push({kind:\"show-page\",page:e.current_page}),r)}function Fn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return Be(Bn);})();", SOURCE_HASH: "6e69736fc03cc5f0" };
package/server/handler.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Page publique de consultation d'un document commercial : /doc/:slug → visionneuse pdf.js qui TRACE
2
2
  // l'ouverture et les PAGES VUES (un lien par destinataire → on sait qui a lu, combien de pages).
3
- // - GET /doc/:slug → HTML visionneuse (pdf.js depuis cdnjs, nonce CSP)
3
+ // - GET /doc/:slug → HTML visionneuse (pdf.js EMBARQUÉ, servi par ?asset=…, nonce CSP)
4
4
  // - GET /doc/:slug?file=1 → stream le PDF depuis le Storage (MÊME ORIGINE → pas de souci CORS pour pdf.js)
5
5
  // - POST /api/doc {slug,event…}→ journalise un événement (open / page / heartbeat) — best-effort
6
6
  const crypto = require("crypto");
@@ -71,14 +71,23 @@ const originOf = (u) => { try { return new URL(u).origin; } catch { return ""; }
71
71
  // atténuation était IMPLICITE — une modification de CSP la rouvrait sans que rien ne le dise.
72
72
  // Les quatre appels à `getDocument` forcent donc désormais `isEvalSupported: false`, en
73
73
  // défense en profondeur : la protection ne dépend plus d'un en-tête écrit ailleurs.
74
- // 2. La montée vers 4.x n'est PAS un changement de numéro : cdnjs ne publie plus que des modules
75
- // ES (`pdf.min.mjs`) à partir de 4.0, alors qu'on charge un script classique et qu'on
76
- // configure le worker à la main. C'est une migration, elle est suivie séparément dans
77
- // docs/AUDIT-2026-08-14-SUIVI.md (P1-3) avec l'embarquement de la bibliothèque, qui règle
78
- // aussi la dépendance CDN sans intégrité (P2-4).
74
+ // 2. La montée vers 4.x+ a été FAITE (0.1.66) : bibliothèque embarquée, modules ES servis par
75
+ // notre route ?asset=…, worker de même origine. L'historique de la contrainte cdnjs vit dans
76
+ // le CHANGELOG ; ce commentaire a longtemps décrit la migration au futur.
79
77
  //
80
78
  // Signalé par un audit externe. Fermer le chemin d'abord, migrer ensuite — dans cet ordre.
81
- const PDFJS = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174";
79
+ // ⚠️ PDF.JS EST EMBARQUÉ (pdfjs-dist, épinglé exact) ET SERVI PAR NOTRE ROUTE `?asset=…`. Trois
80
+ // ans de CDN ont coûté : un tiers dans la CSP, un worker impossible à couvrir par SRI (il n'entre
81
+ // pas par une balise), le ballet d'empreintes qui le contournait, et un épinglage suspendu à ce
82
+ // que cdnjs continuait de publier. Un actif de même origine EST nos octets.
83
+ //
84
+ // ⚠️ LE SCRIPTING PDF EST STRUCTURELLEMENT IMPOSSIBLE, pas désactivé par une option : il exige le
85
+ // sandbox (pdf.sandbox.min.mjs), que nous ne servons ni ne chargeons nulle part. Une option
86
+ // « enableScripting:false » ici serait un placebo — elle appartient au viewer de Mozilla, pas à
87
+ // getDocument. `isEvalSupported:false` reste sur chaque appel : lui est réel (CVE-2024-4367).
88
+ const PDFJS_VERSION = require("pdfjs-dist/package.json").version;
89
+ const PDFJS = "/api/doc?asset=pdf&v=" + PDFJS_VERSION;
90
+ const PDFJS_WORKER = "/api/doc?asset=pdfworker&v=" + PDFJS_VERSION;
82
91
  // ⚠️ VERSION EXACTE, PAS `@2` (constat P2-4). L'étiquette `@2` de jsdelivr suit la dernière 2.x :
83
92
  // la page servait donc, aux visiteurs, le code que Supabase avait publié le matin même — sans que
84
93
  // personne n'ait rien déployé, ni relu, ni pu revenir en arrière. Le jour où elle résolvait vers
@@ -93,7 +102,13 @@ const LEAFLET = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js";
93
102
  // scripts, donc l'intégrité restera hors de portée ici ; épingler la version trimestrielle rend
94
103
  // au moins le changement DÉCIDÉ, et daté. À relever à chaque montée — Google retire les versions
95
104
  // après environ un an.
96
- const MAPS_VERSION = "3.58";
105
+ // ⚠️ UNE VERSION INVALIDE N'EST PAS UNE ERREUR, C'EST UN CANAL PAR DÉFAUT SILENCIEUX. Google ne
106
+ // sert qu'une fenêtre glissante de versions (~4 trimestres) ; en dehors, le paramètre est IGNORÉ
107
+ // et le canal hebdomadaire se charge — l'épinglage devient une illusion sans qu'aucune erreur ne
108
+ // le dise. « 3.58 » a menti ainsi pendant des mois (cinquième audit). À vérifier à chaque
109
+ // trimestre — le commentaire d'à côté doit porter la date du dernier contrôle.
110
+ // Contrôlé le 18/08/2026 : fenêtre servie 3.62 → 3.65.
111
+ const MAPS_VERSION = "3.65";
97
112
 
98
113
  /**
99
114
  * Empreintes des dépendances tierces (constat P2-4).
@@ -116,14 +131,12 @@ const MAPS_VERSION = "3.58";
116
131
  * doivent être comparés par quelqu'un au moins une fois.
117
132
  */
118
133
  const TIERS = {
119
- pdf: { url: PDFJS + "/pdf.min.js", sri: "sha384-/1qUCSGwTur9vjf/z9lmu/eCUYbpOTgSjmpbMQZ1/CtX2v/WcAIKqRv+U1DUCG6e" },
120
134
  // ⚠️ Le worker N'A PAS DE BALISE, donc pas d'attribut `integrity` : il est chargé par pdf.js,
121
135
  // pas par le document. Mais ses octets passent déjà par notre code (ils sont récupérés en texte
122
136
  // puis transformés en blob de même origine, sans quoi le navigateur refuserait un worker
123
137
  // distant) — on les vérifie donc à la main, là où ils passent. Sans ça, l'empreinte de
124
138
  // `pdf.min.js` protégerait la petite moitié et laisserait la grande (1 Mo contre 300 ko) entrer
125
139
  // sans contrôle, alors même qu'elle voit tout le contenu du document.
126
- pdfWorker: { url: PDFJS + "/pdf.worker.min.js", sri: "sha384-SnzOobpRMLXZ52iJvZm/C0fYw0OQemTXzTjIsdsfMcrCtCEe9qgzxTd3RSklO5x2" },
127
140
  supa: { url: SUPAJS, sri: "sha384-qafw21c/iciq0VXsi9FzkfoQv5I/V0iqE4lSNcKXPnW9/UTJLnv5CcN4FHxVLnKg" },
128
141
  leaflet: { url: LEAFLET, sri: "sha384-cxOPjt7s7Iz04uaHJceBmS+qpjv2JkIHNVcuOrM+YHwZOmJGBXI00mdUXEq65HTH" },
129
142
  // ⚠️ UNE FEUILLE DE STYLE TIERCE N'EST PAS INOFFENSIVE, et celle-ci n'était comptée nulle part.
@@ -1744,9 +1757,11 @@ function sendHtml(res, status, html, scriptSrc, imgExtra, frameAncestors) {
1744
1757
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
1745
1758
  res.setHeader("Content-Security-Policy", [
1746
1759
  "default-src 'none'",
1747
- `script-src ${scriptSrc || "'none'"} https://cdnjs.cloudflare.com`,
1748
- "worker-src 'self' blob: https://cdnjs.cloudflare.com",
1749
- `connect-src 'self' https://cdnjs.cloudflare.com${supaOrigin ? " " + supaOrigin : ""}`,
1760
+ // ⚠️ `'none'` DOIT ÊTRE SEUL ou la directive est invalide — « 'none' 'self' » a fait rejeter
1761
+ // la CSP entière par Chrome (vu à la sonde du banc, pas en relisant le code).
1762
+ `script-src ${scriptSrc ? scriptSrc + " 'self'" : "'self'"}`,
1763
+ "worker-src 'self'",
1764
+ `connect-src 'self'${supaOrigin ? " " + supaOrigin : ""}`,
1750
1765
  `img-src 'self' data: blob:${imgExtra ? " " + imgExtra : ""}`,
1751
1766
  `media-src 'self'${supaOrigin ? " " + supaOrigin : ""}`,
1752
1767
  "style-src 'unsafe-inline'",
@@ -2023,12 +2038,11 @@ function viewerHtml(share, nonce, logoUrl, pitch) {
2023
2038
  // En aperçu interne, on embarque de quoi démarrer une présentation live (URL Storage brute + métadonnées).
2024
2039
  // `fileName` : c'est LUI qui dit la nature du document côté page. L'URL publique est
2025
2040
  // `/api/doc?slug=…&file=1`, sans extension — sans ce champ, une image partait dans pdf.js.
2026
- const cfg = jsonPourScript({ brand: PLAYER.branding.name, slug: preview ? "" : share.slug, fileUrl, fileName: share.file_name || "", pdfjs: PDFJS, title, preview, embed, embedded, bot: botOn, botGuided: !preview && !!share.bot_enabled && share.bot_guided !== false, botAv: (!preview && share.bot_enabled && share.bot_avatar) || "", botName: (!preview && share.bot_enabled && share.bot_name) || "", botGreet: (!preview && share.bot_enabled && share.bot_greeting) || "", botGreetDoc: (!preview && share.bot_enabled && share.bot_greeting_doc) || "", dl: share.allow_download !== false, autoPresent: !!share.auto_present, botAnim: share.bot_page_anim !== false, botVoice: !preview && !!share.bot_enabled && !!process.env.ELEVENLABS_API_KEY, vIcOn: ICONS.sound, vIcOff: ICONS.mute, kStyle: (!preview && share.bot_enabled && share.bot_karaoke) || "classic", vLayout: (!preview && share.bot_enabled && share.video_layout) || "", vClips: !preview && !!share.bot_vclips, botVAv: (!preview && share.bot_enabled && share.bot_vphoto) || "", resumeSlug: preview ? (share.resume_slug || "") : "", supaUrl: preview ? (share.supa_url || "") : "", supaKey: preview ? (share.supa_key || "") : "", internal: preview && share.internal_email ? { email: share.internal_email, name: share.presenter_name || "", docId: share.doc_id || "", it: share.internal_token || "" } : null, present: preview ? { url: share.raw_url || "", name: share.file_name || "", title: share.doc_title || "", docId: share.doc_id || "", by: share.presenter_name || "", email: share.internal_email || "", av: share.presenter_avatar || "" } : null });
2041
+ const cfg = jsonPourScript({ brand: PLAYER.branding.name, slug: preview ? "" : share.slug, fileUrl, fileName: share.file_name || "", pdfjs: PDFJS, pdfjsWorker: PDFJS_WORKER, title, preview, embed, embedded, bot: botOn, botGuided: !preview && !!share.bot_enabled && share.bot_guided !== false, botAv: (!preview && share.bot_enabled && share.bot_avatar) || "", botName: (!preview && share.bot_enabled && share.bot_name) || "", botGreet: (!preview && share.bot_enabled && share.bot_greeting) || "", botGreetDoc: (!preview && share.bot_enabled && share.bot_greeting_doc) || "", dl: share.allow_download !== false, autoPresent: !!share.auto_present, botAnim: share.bot_page_anim !== false, botVoice: !preview && !!share.bot_enabled && !!process.env.ELEVENLABS_API_KEY, vIcOn: ICONS.sound, vIcOff: ICONS.mute, kStyle: (!preview && share.bot_enabled && share.bot_karaoke) || "classic", vLayout: (!preview && share.bot_enabled && share.video_layout) || "", vClips: !preview && !!share.bot_vclips, botVAv: (!preview && share.bot_enabled && share.bot_vphoto) || "", resumeSlug: preview ? (share.resume_slug || "") : "", supaUrl: preview ? (share.supa_url || "") : "", supaKey: preview ? (share.supa_key || "") : "", internal: preview && share.internal_email ? { email: share.internal_email, name: share.presenter_name || "", docId: share.doc_id || "", it: share.internal_token || "" } : null, present: preview ? { url: share.raw_url || "", name: share.file_name || "", title: share.doc_title || "", docId: share.doc_id || "", by: share.presenter_name || "", email: share.internal_email || "", av: share.presenter_avatar || "" } : null });
2027
2042
  return `<!doctype html><html lang=fr><head><meta charset=utf-8>
2028
2043
  <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=3,viewport-fit=cover,interactive-widget=resizes-content">
2029
2044
  <meta name=robots content="noindex,nofollow">
2030
2045
  <link rel=icon href="data:,">
2031
- <link rel=preconnect href="https://cdnjs.cloudflare.com" crossorigin>
2032
2046
  <title>${esc(titreOnglet(share))}</title>
2033
2047
  <style>
2034
2048
  :root{--bg:#33312e;--bar:#26241f}
@@ -2223,7 +2237,6 @@ ${LEGAL_CSS}
2223
2237
  ${legalFooter({ tracked: !preview && !!share.slug, sansExpediteur: !share.recipient_email && !share.created_by })}
2224
2238
  ${brandLogo || !brandIntroRuntime ? "" : `<script nonce="${nonce}">(${brandIntroRuntime.toString()})();</script>`}
2225
2239
  <script nonce="${nonce}">${PLAYER_BROWSER_JS}</script>
2226
- ${balise(nonce, TIERS.pdf)}
2227
2240
  ${preview ? `${balise(nonce, TIERS.supa)}
2228
2241
  <script nonce="${nonce}">var LIVECFG=${jsonPourScript({ supaUrl: share.supa_url || "", supaKey: share.supa_key || "", hostAuthKey: cleSessionHote(), liveAuthKey: CLE_SESSION_PLAYER, guestKey: CLE_INVITE })};var GMAPS_KEY=${jsonPourScript((PLAYER.config && PLAYER.config.mapsKey) || "")};${LIVE_JS}
2229
2242
  ${MAP_JS}</script>` : ""}
@@ -2528,7 +2541,10 @@ ${LEGAL_CSS}
2528
2541
  }
2529
2542
  function loadError(m){ var l=document.getElementById('lpct'); if(l){ l.textContent=m; l.className='lerr'; } var b=document.getElementById('lbar'); if(b)b.style.display='none'; }
2530
2543
  function hideLoader(){ var l=document.getElementById('load'); if(l&&!l.classList.contains('hide')){ l.classList.add('hide'); setTimeout(function(){ if(l.parentNode) l.parentNode.removeChild(l); },450); } }
2531
- if(!window.pdfjsLib){ loadError("Impossible de charger la visionneuse."); return; }
2544
+ // (Ici vivait « if(!window.pdfjsLib) return » — sentinelle de l'époque où pdf.js entrait par
2545
+ // une balise AVANT ce script. La bibliothèque s'importe désormais dans le boot lui-même : la
2546
+ // garde ne protégeait plus rien et faisait sortir la visionneuse ENTIÈRE, sans un mot — vu à
2547
+ // la sonde du banc : zéro requête d'actif, zéro erreur, zéro document.)
2532
2548
  function start(){
2533
2549
  // Ouverture journalisée, chrono lancé, écouteurs de visibilité / focus / inactivité posés.
2534
2550
  T.start();
@@ -2569,11 +2585,13 @@ ${LEGAL_CSS}
2569
2585
  if(IS_IMG){ start(); return; }
2570
2586
  loadError("Document non affiche : une dependance n a pas pu etre verifiee.");
2571
2587
  }
2572
- var wsrc=CFG.pdfjs+'/pdf.worker.min.js';
2588
+ // La bibliothèque vient de NOTRE origine, en module ES : l'import qui échoue est un échec de
2589
+ // NOTRE serveur — même refus fermé qu'avant (l'image, elle, n'a pas besoin du worker).
2573
2590
  try{
2574
- Player.viewer.workerBlobUrl(wsrc,'${TIERS.pdfWorker.sri}').then(function(u){
2575
- if(!u){ refuserWorker(); return; }
2576
- pdfjsLib.GlobalWorkerOptions.workerSrc=u; start();
2591
+ import(CFG.pdfjs).then(function(m){
2592
+ window.pdfjsLib=m;
2593
+ pdfjsLib.GlobalWorkerOptions.workerSrc=CFG.pdfjsWorker;
2594
+ start();
2577
2595
  }).catch(refuserWorker);
2578
2596
  }catch(e){ refuserWorker(); }
2579
2597
  // Bord à bord en une-page mobile : chaque millimètre compte, surtout pour un PDF paysage.
@@ -2689,8 +2707,9 @@ ${LEGAL_CSS}
2689
2707
  var tl=document.createElement('div'); tl.className='textLayer';
2690
2708
  tl.style.width=v.width+'px'; tl.style.height=v.height+'px'; tl.style.setProperty('--scale-factor', scale);
2691
2709
  el.appendChild(tl);
2692
- try{ pdfjsLib.renderTextLayer({textContentSource:tc,container:tl,viewport:v}); }
2693
- catch(e){ try{ pdfjsLib.renderTextLayer({textContent:tc,container:tl,viewport:v}); }catch(e2){} }
2710
+ try{ new pdfjsLib.TextLayer({textContentSource:tc,container:tl,viewport:v}).render(); }
2711
+ catch(e){ try{ pdfjsLib.renderTextLayer({textContentSource:tc,container:tl,viewport:v}); }
2712
+ catch(e2){ try{ pdfjsLib.renderTextLayer({textContent:tc,container:tl,viewport:v}); }catch(e3){} } }
2694
2713
  }); }catch(e){}
2695
2714
  }); }
2696
2715
  // ── Assistant IA « présentateur » : chat requête/réponse (bot-start/bot-say) + saut de page piloté ──
@@ -2700,7 +2719,7 @@ ${botOn && botBrowser ? botBrowser.botViewerJs(ICONS) : ""}
2700
2719
  </body></html>`;
2701
2720
  }
2702
2721
 
2703
- // CSP de la page audience (Présenter) : autorise pdf.js (cdnjs), supabase-js (jsdelivr) et la connexion
2722
+ // CSP de la page audience (Présenter) : pdf.js vient de NOTRE origine ; supabase-js (jsdelivr) et la connexion
2704
2723
  // Realtime (https + wss vers le projet Supabase). Plus permissive que la visionneuse, limitée à cette page.
2705
2724
  /**
2706
2725
  * Les origines d'images qu'une page a le droit de charger.
@@ -2758,9 +2777,9 @@ function sendPresentHtml(res, html, nonce, supaUrl, imgExtra, frameAncestors) {
2758
2777
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
2759
2778
  res.setHeader("Content-Security-Policy", [
2760
2779
  "default-src 'none'",
2761
- `script-src 'nonce-${nonce}' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://unpkg.com https://maps.googleapis.com https://maps.gstatic.com`,
2762
- "worker-src 'self' blob: https://cdnjs.cloudflare.com",
2763
- `connect-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://nominatim.openstreetmap.org https://*.googleapis.com https://*.gstatic.com ${supaUrl} ${wss}`,
2780
+ `script-src 'nonce-${nonce}' 'self' https://cdn.jsdelivr.net https://unpkg.com https://maps.googleapis.com https://maps.gstatic.com`,
2781
+ "worker-src 'self'",
2782
+ `connect-src 'self' https://cdn.jsdelivr.net https://nominatim.openstreetmap.org https://*.googleapis.com https://*.gstatic.com ${supaUrl} ${wss}`,
2764
2783
  `img-src 'self' data: blob: https://*.tile.openstreetmap.org https://unpkg.com https://*.googleapis.com https://*.gstatic.com https://*.ggpht.com https://*.googleusercontent.com ${supaUrl}${imgExtra ? " " + imgExtra : ""}`,
2765
2784
  "style-src 'unsafe-inline' https://unpkg.com https://fonts.googleapis.com",
2766
2785
  "font-src https://fonts.gstatic.com data:",
@@ -2788,11 +2807,10 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
2788
2807
  const presenter = esc(pres.presenter_name || "");
2789
2808
  const logo = esc(logoUrl || "");
2790
2809
  const fileUrl = `/api/doc?present=${encodeURIComponent(pres.slug)}&file=1`;
2791
- const cfg = jsonPourScript({ fileUrl, docUrl: pres.file_url, pdfjs: PDFJS, slug: pres.slug, fileName: pres.file_name || "", page: pres.current_page || 1, active: pres.active !== false, content: pres.content || null, supaUrl, supaKey, title: pres.doc_title || pres.file_name || "Document" });
2810
+ const cfg = jsonPourScript({ fileUrl, docUrl: pres.file_url, pdfjs: PDFJS, pdfjsWorker: PDFJS_WORKER, slug: pres.slug, fileName: pres.file_name || "", page: pres.current_page || 1, active: pres.active !== false, content: pres.content || null, supaUrl, supaKey, title: pres.doc_title || pres.file_name || "Document" });
2792
2811
  return `<!doctype html><html lang=fr><head><meta charset=utf-8>
2793
2812
  <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=3">
2794
2813
  <meta name=robots content="noindex,nofollow">
2795
- <link rel=preconnect href="https://cdnjs.cloudflare.com" crossorigin>
2796
2814
  <title>${esc(PLAYER.branding.title(pres.doc_title || pres.file_name || "Document", "Présentation"))}</title>
2797
2815
  <style>
2798
2816
  :root{--bg:#23211e;--bar:#1a1916}
@@ -2858,7 +2876,6 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
2858
2876
  ${PLAYER.branding.poweredBy ? `<div class=brand>Propulsé par ${esc(PLAYER.branding.poweredBy)}</div>` : ""}
2859
2877
  ${legalFooter({ tracked: true })}
2860
2878
  <script nonce="${nonce}">${PLAYER_BROWSER_JS}</script>
2861
- ${balise(nonce, TIERS.pdf)}
2862
2879
  ${balise(nonce, TIERS.supa)}
2863
2880
  <script nonce="${nonce}">
2864
2881
  (function(){
@@ -2894,11 +2911,11 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
2894
2911
  pageEl.innerHTML=''; pageEl.appendChild(cv);
2895
2912
  });
2896
2913
  }
2897
- var wsrc=CFG.pdfjs+'/pdf.worker.min.js';
2898
2914
  function boot(){
2899
- Player.viewer.workerBlobUrl(wsrc,'${TIERS.pdfWorker.sri}').then(function(u){
2900
- if(!u){ window.__workerRefuse=1; var _e=document.getElementById('pg'); if(_e)_e.textContent="Document non affiche : une dependance n a pas pu etre verifiee."; return; }
2901
- pdfjsLib.GlobalWorkerOptions.workerSrc=u; load();
2915
+ import(CFG.pdfjs).then(function(m){
2916
+ window.pdfjsLib=m;
2917
+ pdfjsLib.GlobalWorkerOptions.workerSrc=CFG.pdfjsWorker;
2918
+ load();
2902
2919
  }).catch(function(){ window.__workerRefuse=1; var _e=document.getElementById('pg'); if(_e)_e.textContent="Document non affiche : une dependance n a pas pu etre verifiee."; });
2903
2920
  }
2904
2921
  // ⚠️ UNE PRÉSENTATION PEUT PORTER UNE IMAGE, ET CETTE VUE NE LE SAVAIT PAS. Le bouton
@@ -2968,7 +2985,8 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
2968
2985
  CFG.fileUrl='/api/doc?present='+encodeURIComponent(CFG.slug)+'&file=1&v='+encodeURIComponent(row.updated_at||String(row.current_page||1));
2969
2986
  load();
2970
2987
  }
2971
- if(window.pdfjsLib) boot(); else { var s=document.querySelector('script[src*="pdf.min.js"]'); if(s)s.addEventListener('load',boot); }
2988
+ // La balise pdf.min.js n'existe plus : boot() importe lui-même la bibliothèque.
2989
+ boot();
2972
2990
  window.__refit=function(){ if(ready) show(cur); };
2973
2991
  var _rzA; window.addEventListener('resize',function(){ clearTimeout(_rzA); _rzA=setTimeout(function(){ if(ready) show(cur); },140); });
2974
2992
  // Ce que l'audience fait d'un état reçu — player/src/presentation-state.ts, testé.
@@ -3582,24 +3600,43 @@ async function handler(req, res) {
3582
3600
  const dejaLa = await PLAYER.db.request(
3583
3601
  `commercial_doc_shares?doc_id=eq.${encodeURIComponent(docId)}&created_by=is.null&recipient_email=is.null${filtreAtteste}&select=slug&limit=1`,
3584
3602
  );
3603
+ const cleHote = `hote:${docId}|${atteste || ""}`;
3585
3604
  if (Array.isArray(dejaLa) && dejaLa[0]) {
3605
+ // Réemploi d'une ligne historique (sans clé) : on lui POSE la clé au passage — les
3606
+ // demandes suivantes la trouveront par l'unicité, et les doublons d'avant 0011
3607
+ // s'éteignent d'eux-mêmes faute d'être resservis.
3608
+ const cleDispo = await require("./schema").attendue("liensUniques");
3586
3609
  await PLAYER.db.request(`commercial_doc_shares?slug=eq.${encodeURIComponent(dejaLa[0].slug)}`, {
3587
3610
  method: "PATCH", headers: { Prefer: "return=minimal" },
3588
- body: { doc_title: body.docTitle || null, file_url: String(body.fileUrl), file_name: body.fileName || null, revoked: false },
3611
+ body: { doc_title: body.docTitle || null, file_url: String(body.fileUrl), file_name: body.fileName || null, revoked: false, ...(cleDispo ? { idem_key: cleHote } : {}) },
3589
3612
  });
3590
3613
  return jd(200, { ok: true, slug: dejaLa[0].slug, reused: true });
3591
3614
  }
3592
3615
  // `createdBy` reste NUL : personne ne l'a créé. Le filtre « mes liens » compare
3593
3616
  // `created_by=eq.<email>`, qui exclut les NUL — ce lien n'apparaît donc dans la liste
3594
3617
  // de personne, et reste visible en administration (`list.all`, qui ne filtre pas).
3595
- const neuf = await createShare({
3596
- brandKey: body.brandKey, docId, docTitle: body.docTitle, fileUrl: body.fileUrl,
3597
- fileName: body.fileName, createdBy: null, attestedRecipientEmail: atteste || null,
3598
- bot: body.bot, botScript: body.botScript,
3599
- guided: body.guided, profileId: body.profileId, allowDownload: body.allowDownload,
3600
- videoLayout: body.videoLayout, logo: body.logo, logoDark: body.logoDark,
3601
- });
3602
- return jd(200, { ok: true, slug: neuf.slug, reused: false });
3618
+ // ⚠️ LA LECTURE-PUIS-ÉCRITURE NE PROMET RIEN : deux demandes dans la même seconde
3619
+ // passaient toutes deux le « déjà là ? » et DEUX liens naissaient — les statistiques du
3620
+ // document se fragmentent entre eux. La contrainte (0011) promet ; le 409 qu'elle rend
3621
+ // est une CONFIRMATION : l'autre demande a gagné, on relit son lien et on le rend
3622
+ // `reused` — même règle que le renvoi d'un message (0005).
3623
+ try {
3624
+ const neuf = await createShare({
3625
+ brandKey: body.brandKey, docId, docTitle: body.docTitle, fileUrl: body.fileUrl,
3626
+ fileName: body.fileName, createdBy: null, attestedRecipientEmail: atteste || null,
3627
+ bot: body.bot, botScript: body.botScript,
3628
+ guided: body.guided, profileId: body.profileId, allowDownload: body.allowDownload,
3629
+ videoLayout: body.videoLayout, logo: body.logo, logoDark: body.logoDark,
3630
+ idemKey: cleHote,
3631
+ });
3632
+ return jd(200, { ok: true, slug: neuf.slug, reused: false });
3633
+ } catch (erreur) {
3634
+ if (!String((erreur && erreur.message) || "").includes("409")) throw erreur;
3635
+ 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 */ }
3636
+ const gagnant = await PLAYER.db.request(`commercial_doc_shares?idem_key=eq.${encodeURIComponent(cleHote)}&select=slug&limit=1`);
3637
+ if (!Array.isArray(gagnant) || !gagnant[0]) throw erreur; // 409 d'autre chose : on ne l'invente pas
3638
+ return jd(200, { ok: true, slug: gagnant[0].slug, reused: true });
3639
+ }
3603
3640
  }
3604
3641
 
3605
3642
  const u = await PLAYER.identity.verifyToken(req.headers.authorization);
@@ -3637,12 +3674,23 @@ async function handler(req, res) {
3637
3674
  const docId = String(body.docId || "");
3638
3675
  if (!docId || !body.fileUrl) return jd(400, { ok: false, error: "docId/fileUrl requis" });
3639
3676
  const ex = await PLAYER.db.request(`commercial_doc_shares?doc_id=eq.${encodeURIComponent(docId)}&is_test=eq.true&select=slug&limit=1`);
3677
+ const cleTest = `repetition:${docId}`;
3640
3678
  if (Array.isArray(ex) && ex[0]) {
3641
- 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 } });
3679
+ const cleDispo = await require("./schema").attendue("liensUniques");
3680
+ 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 } : {}) } });
3642
3681
  return jd(200, { ok: true, slug: ex[0].slug });
3643
3682
  }
3644
- 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 });
3645
- return jd(200, { ok: true, slug: t.slug });
3683
+ // Deux ouvertures simultanées de la répétition : la contrainte tranche, le perdant relit.
3684
+ try {
3685
+ 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 });
3686
+ return jd(200, { ok: true, slug: t.slug });
3687
+ } catch (erreur) {
3688
+ if (!String((erreur && erreur.message) || "").includes("409")) throw erreur;
3689
+ 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 */ }
3690
+ const gagnant = await PLAYER.db.request(`commercial_doc_shares?idem_key=eq.${encodeURIComponent(cleTest)}&select=slug&limit=1`);
3691
+ if (!Array.isArray(gagnant) || !gagnant[0]) throw erreur;
3692
+ return jd(200, { ok: true, slug: gagnant[0].slug, reused: true });
3693
+ }
3646
3694
  }
3647
3695
  if (body.action === "docshare.revoke") {
3648
3696
  await revokeShare(String(body.slug || ""));
@@ -3860,14 +3908,34 @@ async function handler(req, res) {
3860
3908
  // AUCUN DOCUMENT DEMANDÉ. Ni slug, ni présentation, ni aperçu, ni carte d'identité — il n'y a
3861
3909
  // rien à afficher, et ce n'est pas un refus. Le dire franchement évite qu'un intégrateur
3862
3910
  // cherche un lien révoqué là où il lui manque un paramètre.
3863
- if (req.method === "GET" && !slug && !q.present && !q.preview && !q.contract) {
3911
+ if (req.method === "GET" && !slug && !q.present && !q.preview && !q.contract && !q.asset) {
3864
3912
  res.statusCode = 400;
3865
3913
  res.setHeader("Content-Type", "text/plain; charset=utf-8");
3866
- res.end("Aucun document demandé. Attendu : ?slug=… , ?present=… , ?preview=1 ou ?contract=1.\n" +
3914
+ res.end("Aucun document demandé. Attendu : ?slug=… , ?present=… , ?preview=1, ?contract=1 ou ?asset=….\n" +
3867
3915
  "Si vous intégrez le player, vérifiez que la plateforme fournit les paramètres de requête.");
3868
3916
  return;
3869
3917
  }
3870
3918
 
3919
+ // ── ACTIFS PDF.JS (`?asset=pdf` / `?asset=pdfworker`) ────────────────────────────────────
3920
+ // La bibliothèque est EMBARQUÉE (pdfjs-dist, épinglée exacte) et servie depuis notre origine.
3921
+ // Ce que ça ferme d'un coup : le tiers CDN dans la CSP, le script sans SRI possible (le worker
3922
+ // n'entre pas par une balise), et le ballet d'empreintes qui l'entourait — un actif de même
3923
+ // origine EST nos octets. Public et sans base, comme la carte : un actif qui exigerait une
3924
+ // session casserait la page avant qu'elle existe. L'URL porte la version → cache immuable.
3925
+ if (req.method === "GET" && (String(q.asset || "") === "pdf" || String(q.asset || "") === "pdfworker")) {
3926
+ const fichier = q.asset === "pdf" ? "pdfjs-dist/build/pdf.min.mjs" : "pdfjs-dist/build/pdf.worker.min.mjs";
3927
+ let octets = null;
3928
+ try { octets = require("node:fs").readFileSync(require.resolve(fichier)); } catch { /* dépendance absente */ }
3929
+ if (!octets) { res.statusCode = 404; res.end("actif indisponible"); return; }
3930
+ res.statusCode = 200;
3931
+ res.setHeader("Content-Type", "text/javascript; charset=utf-8");
3932
+ res.setHeader("X-Content-Type-Options", "nosniff");
3933
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
3934
+ res.setHeader("Content-Length", String(octets.length));
3935
+ res.end(octets);
3936
+ return;
3937
+ }
3938
+
3871
3939
  // ── CARTE D'IDENTITÉ (`?contract=1`) ─────────────────────────────────────────────────────
3872
3940
  // La règle 4 du contrat demande à l'hôte d'épingler la version qu'il vise et de le VÉRIFIER.
3873
3941
  // Sans point d'interrogation, cette règle était une intention : un hôte ne pouvait pas écrire
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;
@@ -329,9 +335,6 @@ alter table public.doc_bot_sessions enable row level security;
329
335
  -- n'est pas un détail : il pousse un mainteneur à « réparer » un différé qui n'existe plus, en
330
336
  -- ajoutant précisément la politique de lecture que ce fichier refuse.)
331
337
  --
332
- -- La publication de la table reste utile aux INSTANCES HISTORIQUES encore sur `postgres_changes`,
333
- -- et inoffensive ici : publiée sans politique de lecture, elle ne livre rien à personne.
334
- --
335
338
  -- ⚠️ Ne mettez de toute façon jamais de donnée confidentielle dans un message de chat ni dans un
336
339
  -- nom d'auteur.
337
340
  -- ⚠️ AUCUNE TABLE DU PLAYER N'EST PUBLIÉE DANS `supabase_realtime`, ET C'EST UN CHOIX. Le direct
@@ -403,6 +406,8 @@ alter table public.doc_presentations
403
406
  add column if not exists write_seq bigint not null default 0;
404
407
  alter table public.doc_presentation_messages
405
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;
406
411
  -- Une base née d'un init.sql d'avant la 0008 porte un NOT NULL qui rend la clôture impossible.
407
412
  alter table public.doc_presentations
408
413
  alter column control_hash drop not null;
@@ -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;