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 +1 -1
- package/docs/HOST-CONTRACT.md +8 -0
- package/package.json +1 -1
- package/server/browser.generated.js +1 -1
- package/server/handler.js +67 -19
- package/server/presentations.js +42 -8
- package/server/schema.js +5 -0
- package/server/shares.js +6 -1
- package/supabase/init.sql +17 -6
- package/supabase/migrations/0007-archive-scellee.sql +5 -4
- package/supabase/migrations/0010-archive-verrou-share.sql +38 -0
- package/supabase/migrations/0011-liens-uniques.sql +27 -0
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
|
|
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/docs/HOST-CONTRACT.md
CHANGED
|
@@ -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.
|
|
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,\"&\").replace(/</g,\"<\").replace(/>/g,\">\").replace(/\"/g,\""\")}function Ce(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function ee(e,n){return e?`<img src=\"${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,\"&\").replace(/</g,\"<\").replace(/>/g,\">\").replace(/\"/g,\""\")}function Ce(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function ee(e,n){return e?`<img src=\"${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
|
-
|
|
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
|
|
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
|
|
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.
|
|
351
|
-
// (« les autres l'ajoutent chez eux ») : il a fait dériver
|
|
352
|
-
//
|
|
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
|
|
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
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3639
|
-
|
|
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
|
package/server/presentations.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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`
|
|
348
|
-
--
|
|
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
|
|
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
|
-
-- ⚠️
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
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;
|