discovery-media-player 0.1.50 → 0.1.52
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/context/standalone.js +34 -19
- package/docs/HOST-CONTRACT.md +11 -2
- package/package.json +1 -1
- package/server/browser.generated.js +1 -1
- package/server/handler.js +128 -24
- package/server/presentations.js +166 -9
- package/supabase/migrations/0004-limites-atomiques.sql +55 -0
package/context/standalone.js
CHANGED
|
@@ -164,6 +164,13 @@ function creerLimites(db, journal) {
|
|
|
164
164
|
return true;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
let deja = "";
|
|
168
|
+
function prevenirUneFois(message) {
|
|
169
|
+
if (deja === message) return;
|
|
170
|
+
deja = message;
|
|
171
|
+
try { journal.capture(new Error(message), { route: "limits" }); } catch { /* jamais bloquant */ }
|
|
172
|
+
}
|
|
173
|
+
|
|
167
174
|
async function tablePresente() {
|
|
168
175
|
if (partageDisponible !== null) return partageDisponible;
|
|
169
176
|
try {
|
|
@@ -191,28 +198,36 @@ function creerLimites(db, journal) {
|
|
|
191
198
|
if (PREFIXES_LOCAUX.some((p) => String(cle).startsWith(p))) return true;
|
|
192
199
|
if (!(await tablePresente())) return true;
|
|
193
200
|
|
|
194
|
-
// Étage 2 : le compte partag
|
|
195
|
-
//
|
|
201
|
+
// Étage 2 : le compte partagé, en UN SEUL GESTE.
|
|
202
|
+
//
|
|
203
|
+
// ⚠️ IL EN FAISAIT TROIS — lire, calculer, écrire — et sous charge, c'est-à-dire exactement
|
|
204
|
+
// quand une limite sert à quelque chose, plusieurs requêtes lisaient le même compte et
|
|
205
|
+
// écrivaient chacune « ce compte + 1 ». Vingt requêtes simultanées sur une limite de dix
|
|
206
|
+
// passaient toutes. Une limite qui ne tient que lorsque personne ne pousse ne limite rien.
|
|
207
|
+
//
|
|
208
|
+
// ⚠️ Échec OUVERT, et c'est délibéré : une limite injoignable ne doit pas empêcher de lire un
|
|
209
|
+
// document. Un 429 raté coûte moins cher qu'une visionneuse morte. Mais l'ouverture se DIT,
|
|
210
|
+
// une fois — un compteur qui ne compte plus en silence est pire qu'un compteur absent.
|
|
196
211
|
try {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const vivante = ligne && new Date(ligne.expires_at).getTime() > Date.now();
|
|
201
|
-
if (!vivante) {
|
|
202
|
-
await db.request("player_rate_limits", {
|
|
203
|
-
method: "POST",
|
|
204
|
-
headers: { Prefer: "resolution=merge-duplicates,return=minimal" },
|
|
205
|
-
body: [{ key: String(cle), count: 1, expires_at: fin }],
|
|
206
|
-
});
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
if (Number(ligne.count || 0) >= max) return false;
|
|
210
|
-
await db.request(`player_rate_limits?key=eq.${encodeURIComponent(cle)}`, {
|
|
211
|
-
method: "PATCH", headers: { Prefer: "return=minimal" },
|
|
212
|
-
body: { count: Number(ligne.count || 0) + 1 },
|
|
212
|
+
const r = await db.request("rpc/player_rate_limit_bump", {
|
|
213
|
+
method: "POST",
|
|
214
|
+
body: { p_key: String(cle), p_max: max, p_window_seconds: fenetreSecondes },
|
|
213
215
|
});
|
|
216
|
+
const ligne = Array.isArray(r) ? r[0] : r;
|
|
217
|
+
if (ligne && typeof ligne.autorise === "boolean") return ligne.autorise;
|
|
218
|
+
// Réponse d'une forme qu'on ne comprend pas : on n'invente pas un verdict.
|
|
219
|
+
prevenirUneFois("compteurs de débit : réponse inattendue de player_rate_limit_bump");
|
|
214
220
|
return true;
|
|
215
|
-
} catch {
|
|
221
|
+
} catch (erreur) {
|
|
222
|
+
// ⚠️ LA FONCTION PEUT SIMPLEMENT NE PAS ÊTRE LÀ — un hôte qui n'a pas appliqué la migration
|
|
223
|
+
// reçoit un 404 de PostgREST, indiscernable ici d'une panne. Dans les deux cas on laisse
|
|
224
|
+
// passer ; dans les deux cas on le dit, en NOMMANT le fichier à appliquer. C'est la règle
|
|
225
|
+
// du chemin de migration : dégrader, jamais casser, et ne jamais dégrader en silence.
|
|
226
|
+
prevenirUneFois(
|
|
227
|
+
"compteurs de débit non atomiques : appliquez supabase/migrations/0004-limites-atomiques.sql. "
|
|
228
|
+
+ "Sans elle, plusieurs requêtes simultanées peuvent dépasser la limite ensemble. "
|
|
229
|
+
+ "(" + ((erreur && erreur.message) || erreur) + ")",
|
|
230
|
+
);
|
|
216
231
|
return true;
|
|
217
232
|
}
|
|
218
233
|
},
|
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -76,8 +76,17 @@ This matters because nothing announced the difference: on serverless, several ex
|
|
|
76
76
|
parallel and start cold, so a limit of 120/hour allowed 120 **per instance**. It existed, it
|
|
77
77
|
reassured, and it bounded a fraction of what it claimed.
|
|
78
78
|
|
|
79
|
-
Requires `supabase/migrations/0003-limites-partagees.sql
|
|
80
|
-
|
|
79
|
+
Requires `supabase/migrations/0003-limites-partagees.sql` **and**
|
|
80
|
+
`0004-limites-atomiques.sql`. **Until they are applied, nothing breaks**: counting falls back —
|
|
81
|
+
to memory without the first, to a non-atomic read-modify-write without the second — and the host is
|
|
82
|
+
told once, by name.
|
|
83
|
+
|
|
84
|
+
⚠️ **The second one is what makes the counter hold under load.** Reading, computing and writing as
|
|
85
|
+
three steps means that several simultaneous requests read the same count and each write "that count
|
|
86
|
+
plus one" — so they cross the ceiling together, precisely when a limit is supposed to matter. The
|
|
87
|
+
atomic increment is **not expressible in REST** (`on conflict do update set count = count + 1` has
|
|
88
|
+
to name the column on both sides), which is why this one operation, and only this one, goes through
|
|
89
|
+
a database function.
|
|
81
90
|
|
|
82
91
|
Two deliberate exceptions, both written next to the code:
|
|
83
92
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.52",
|
|
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,Ue=Object.defineProperties,De=Object.getOwnPropertyDescriptor,We=Object.getOwnPropertyDescriptors,Ve=Object.getOwnPropertyNames,ye=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,B=(e,n)=>{for(var t in n||(n={}))Me.call(n,t)&&ve(e,t,n[t]);if(ye)for(var t of ye(n))ze.call(n,t)&&ve(e,t,n[t]);return e},xe=(e,n)=>Ue(e,We(n));var v=(e,n)=>{for(var t in n)C(e,t,{get:n[t],enumerable:!0})},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 je=e=>Fe(C({},\"__esModule\",{value:!0}),e);var Fn={};v(Fn,{bridge:()=>L,cadence:()=>N,chat:()=>H,live:()=>$,presentation:()=>U,presentationState:()=>W,tracking:()=>O,viewer:()=>D});var L={};v(L,{onHostMessage:()=>en,onPlayerMessage:()=>Je,parseHostMessage:()=>Te,parsePlayerMessage:()=>we,sendToHost:()=>qe,sendToPlayer:()=>Qe,toWire:()=>Y});var K=\"3dd-doc-\",Ge=/^[A-Za-z0-9_-]{1,64}$/,Be=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Ke=[\"embed-denied\"],Ye=[\"present-invite\",\"present-handover\",\"present-switch\"],Ze=[\"handover-done\"];function Y(e){let n={type:K+e.type};return\"slug\"in e&&e.slug&&(n.slug=e.slug),\"reason\"in e&&e.reason&&(n.reason=String(e.reason).slice(0,40)),n}function _e(e,n,t){if(!e||typeof e!=\"object\")return null;let r=e.type;if(typeof r!=\"string\"||!r.startsWith(K))return null;let o=r.slice(K.length);if(n.includes(o))return{type:o};if(Ke.includes(o)){let a=e.reason;return{type:o,reason:/^[a-z-]{1,40}$/.test(String(a||\"\"))?String(a):\"unknown\"}}if(!t.includes(o))return null;let s=e.slug;return typeof s!=\"string\"||!Ge.test(s)?null:{type:o,slug:s}}function we(e){return _e(e,Be,Ye)}function Te(e){return _e(e,Ze,[])}function qe(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(Y(e),\"*\")}catch(t){}}function Qe(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(Y(n),\"*\")}catch(r){}}function Xe(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function Je(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=s=>{if(!Xe(t,s.source))return;let a=we(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 l=Te(a.data);l&&e(l)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var O={};v(O,{createTracker:()=>ln});var N={};v(N,{PRESENTER_ACTIONS_PER_HOUR:()=>X,PRESENT_CACHE_MS:()=>rn,PRESENT_QUOTA_MARGIN:()=>Ae,PRESENT_QUOTA_PER_HOUR:()=>on,PRESENT_READS_PER_HOUR:()=>Re,PRESENT_READ_BURST:()=>Ie,PRESENT_READ_COALESCE_MS:()=>Pe,PRESENT_RESYNC_MS:()=>Se,PRESENT_SIGNAL_BUDGET_PER_HOUR:()=>tn,PRESENT_WRITE_TIMEOUT_MS:()=>J,READERS_PER_EGRESS:()=>q,RESYNC_READS_PER_HOUR:()=>ke,SESSION_IDLE_MS:()=>Q,SESSION_INTERVAL_MS:()=>Z,SESSION_QUOTA_PER_HOUR:()=>nn,SESSION_WRITES_PER_HOUR:()=>Ee});var Z=12e3,Ee=Math.ceil(300),q=25,nn=Ee*q,Q=18e4,Se=25e3,ke=Math.ceil(36e5/Se),X=720,Re=ke+X,tn=X,Ie=20,J=1e4,Pe=400,rn=Pe,Ae=4,on=(Re+Ie)*q*Ae;var sn=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function an(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(16));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}return Math.random().toString(36).slice(2)+Date.now().toString(36)}function un(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;o&&typeof o.sendBeacon==\"function\"?o.sendBeacon(e,new Blob([r],{type:\"application/json\"})):n.fetch(e,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:r,keepalive:!0})}catch(r){}}}function ln(e={}){var ge,be;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",s=(ge=e.idleMs)!=null?ge:Q,a=(be=e.sessionEveryMs)!=null?be:12e3,l=e.slug||\"\",d=e.internal||null,m=e.sessionId||an(),p=e.send||un(o,n),c={},u=0,g=0,f=0,M=0,y=null,E=r(),S=!1,V=!1,P=[],He=()=>!!l||!!d,ce=i=>{if(He())try{p(i)}catch(b){}},z=()=>!!t&&t.visibilityState===\"visible\"&&!S,de=i=>{let b=r()-i,_=Math.max(0,E-i)+s;return Math.max(0,Math.min(b,_))},F=()=>{y!=null&&u>0&&(c[u]=(c[u]||0)+de(y)/1e3),y=z()?r():null},pe=()=>{F(),y=null},j=()=>{y==null&&z()&&(y=r())},G=()=>{let i=0;for(let b in c)i+=c[b];return y!=null&&(i+=de(y)/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,j())},me=(i,b)=>{d||ce({slug:l,event:i,page:b||g,maxPage:f,seconds:G(),sessionId:m})},A=()=>{F();let i={event:\"session\",sessionId:m,numPages:M,maxPage:f,totalSeconds:G(),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=l,ce(i)},R=(i,b,_,he)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(b,_,he),P.push(()=>{i.removeEventListener(b,_,he)}))};return{sessionId:m,totalSeconds:G,maxPage:()=>f,pageTimes:fe,start(){if(V)return;V=!0,me(\"open\",1),u=1,E=r(),y=z()?r():null,R(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(pe(),A()):(k(),j())}),R(n,\"focus\",()=>{k(),j()});for(let _ of sn)R(n,_,k,{passive:!0});R(e.scrollElement,\"scroll\",k,{passive:!0}),R(n,\"beforeunload\",A);let i=n.setInterval(()=>{!S&&r()-E>s&&(S=!0,pe())},5e3),b=n.setInterval(A,a);P.push(()=>{n.clearInterval(i),n.clearInterval(b)})},setPage(i){!i||i===g||(E=r(),F(),u=i,g=i,i>f&&(f=i,me(\"page\",i)))},setPageCount(i){M=i||0},noteActivity:k,flush:A,stop(){for(;P.length;){let i=P.pop();try{i==null||i()}catch(b){}}V=!1}}}var $={};v($,{STORAGE_KEYS:()=>x,attendeeKey:()=>fn,authorToken:()=>mn,avatarHtml:()=>ee,canModerate:()=>oe,createBudget:()=>Mn,createFileEcritures:()=>xn,createScheduler:()=>yn,escapeHtml:()=>h,fetchBorne:()=>vn,flattenPresence:()=>cn,formatMessageBody:()=>ne,initials:()=>Ce,isMentioned:()=>gn,isMine:()=>w,presenceId:()=>_n,reactorId:()=>re,shouldNotify:()=>bn,unreadLabel:()=>hn});var x={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\",presenceId:\"3dd-present-uid\"};function h(e){return String(e==null?\"\":e).replace(/&/g,\"&\").replace(/</g,\"<\").replace(/>/g,\">\").replace(/\"/g,\""\")}function Ce(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function ee(e,n){return e?`<img src=\"${h(e)}\" alt=\"\">`:h(Ce(n))}function ne(e){let n=h(e);return n=n.replace(/(https?:\\/\\/[^\\s<]+)/g,t=>`<a href=\"${t}\" target=_blank rel=noreferrer class=cm-link>${t}</a>`),n=n.replace(/(^|\\s)@([\\p{L}0-9_'.-]+)/gu,(t,r,o)=>`${r}<span class=cm-mention>@${o}</span>`),n}function cn(e){let n=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(x.attendeeKey);if(r)return r;let o=`anon-${t()}`;return e.setItem(x.attendeeKey,o),o}catch(r){return`anon-${n}`}}function te(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(24));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}try{console.warn(\"[player] crypto indisponible : jeton d'auteur affaibli\")}catch(n){}return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}function mn(e,n=te){let t=dn(e,x.authorToken);if(t)return t;let r=n();return pn(e,x.authorToken,r),r}function re(e){return String(e&&(e.email||e.name)||\"\").toLowerCase()}function w(e,n){return!e||!n?!1:e.author_email&&n.email?e.author_email===n.email:!e.author_email&&!n.email?e.author_name===n.name:!1}function oe(e){return!!(e&&e.role===\"presenter\")}function gn(e,n){if(!n||!n.name)return!1;let t=(n.name.split(/\\s+/)[0]||\"\").toLowerCase();return t?String(e&&e.body||\"\").toLowerCase().includes(`@${t}`)&&!w(e,n):!1}function bn(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||w(n,t)?!1:o}function hn(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function yn(e,n={}){var g;let t=(g=n.minMs)!=null?g:300,r=n.now||(()=>Date.now()),o=n.setTimer||((f,M)=>setTimeout(f,M)),s=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&&(s(m),m=null)}}}function vn(e,n,t=J,r={}){let o=r.fetch||(typeof fetch==\"function\"?fetch:void 0);if(!o)return Promise.reject(new Error(\"fetch indisponible\"));let s=r.setTimer||((u,g)=>setTimeout(u,g)),a=r.clearTimer||(u=>clearTimeout(u)),l=null;try{l=new AbortController}catch(u){}let d=l?xe(B({},n||{}),{signal:l.signal}):B({},n||{}),m=null,p=new Promise((u,g)=>{m=s(()=>{try{l==null||l.abort()}catch(f){}g(new Error(\"delai\"))},t)}),c=()=>{try{a(m)}catch(u){}};return Promise.race([o(e,d),p]).then(u=>(c(),u),u=>{throw c(),u})}function Mn(e){let n=e.now||(()=>Date.now()),t=Math.max(1,e.rafale),r=t,o=n();return{prendre(){let 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,l=!1;function d(){if(s||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),s=!0,a=t(),Promise.resolve().then(u.tache).then(g=>u.regler(g),()=>u.regler(null)).then(()=>{s=!1,d()})}return{poser(p,c){return new Promise(u=>{let g=o.get(p);o.set(p,{tache:c,regler:f=>{g&&g.regler(f),u(f)}}),d()})},enAttente:()=>o.size}}function _n(e,n=te){try{if(e){let t=e.getItem(x.presenceId);if(t)return t;let r=`p-${n()}`;return e.setItem(x.presenceId,r),r}}catch(t){}return`p-${n()}`}var H={};v(H,{messageClassName:()=>Tn,renderActions:()=>Oe,renderAttachment:()=>Ne,renderMessage:()=>wn,renderReactions:()=>Le});function Le(e,n){let t=e&&e.reactions||{},r=re(n),o=\"\";for(let s in t){let a=t[s]||[];if(!a.length)continue;let l=a.indexOf(r)>=0;o+=`<button class=\"re-chip${l?\" on\":\"\"}\" data-e=\"${h(s)}\">${h(s)} ${a.length}</button>`}return o}function Ne(e){if(!e||!e.url)return\"\";let n=h(e.url);if(e.kind===\"image\")return`<a class=cm-att href=\"${n}\" target=_blank rel=noreferrer><img src=\"${n}\" alt=\"\" loading=lazy></a>`;let t=h(e.name||\"Document.pdf\");return`<a class=cm-att-pdf href=\"${n}\" target=_blank rel=noreferrer data-pdf=\"${n}\"><span class=cm-att-ph></span><span class=cm-pdflabel>${t}</span></a>`}function Oe(e,n){let t=w(e,n.me),r=`<button class=cm-react title=\"R\\xE9agir\">${n.reactIcon||\"\"}</button><button class=cm-reply title=\"R\\xE9pondre\">\\u21A9</button>`;return t&&(r+='<button class=cm-edit title=\"Modifier\">\\u270E</button>'),(t||oe(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function wn(e,n){if(e.deleted)return'<span class=a></span><span class=b><div class=\"txt cm-del\">Message supprim\\xE9</div></span>';let t=e.reply_to?`<div class=cm-q><b>${h(e.reply_name||\"\")}</b> ${h(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${ne(e.body)}${r}</div>`:\"\",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>${h(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 Tn(e,n,t){return\"cm\"+(w(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var U={};v(U,{DEFAULT_CENTER:()=>ae,DEFAULT_ZOOM:()=>ue,MAP_TYPES:()=>I,cycleMapType:()=>En,initialMapContent:()=>kn,mapTypeLabel:()=>Sn,sanitizeContent:()=>le});var I=[\"roadmap\",\"satellite\",\"hybrid\"],ae=[46.6,2.5],ue=6;function T(e){let n=Number(e);return Number.isFinite(n)?n:null}function ie(e){if(!Array.isArray(e))return null;let n=T(e[0]),t=T(e[1]);return n!=null&&t!=null?[n,t]:null}var se=(e,n,t)=>Math.max(n,Math.min(t,e));function le(e){if(!e||typeof e!=\"object\")return null;let n=e;if(n.kind===\"pdf\"||n.kind==null)return null;if(n.kind===\"streetview\"){let t=ie(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:T(r.heading)||0,pitch:se(T(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:se(T(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:ie(n.center)||[...ae],zoom:se(Math.trunc(T(n.zoom)||ue),1,21),marker:ie(n.marker),mapType:I.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function En(e){let n=I.indexOf(e);return I[(n+1)%I.length]}function Sn(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function kn(){return{kind:\"map\",center:[...ae],zoom:ue,marker:null,mapType:null,label:null}}var D={};v(D,{CURRENT_PAGE_MARGIN:()=>Ln,DEFAULT_ASPECT:()=>Pn,MAX_ZOOM:()=>In,MIN_PAGE_WIDTH:()=>An,MIN_ZOOM:()=>Rn,PRERENDER_MARGIN:()=>Cn,arrowState:()=>$n,averageColor:()=>Dn,clampPage:()=>$e,clampZoom:()=>Nn,fitWidth:()=>Un,isImageDocument:()=>Wn,progressPercent:()=>On});var Rn=.5,In=3,Pn=1.35,An=280,Cn=\"500px 0px\",Ln=\"-48% 0px -48% 0px\";function Nn(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function $e(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function On(e,n){return n?Math.round($e(e,n)/n*100):0}function $n(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var Hn=28;function Un(e){var a;let n=(a=e.zoom)!=null?a:1,t=Math.max(280,e.containerWidth||900)*n;if(!e.onePage)return t;let r=e.aspect||1.35,s=((e.containerHeight||600)-Hn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function Dn(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let 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 Wn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var W={};v(W,{presentationTransition:()=>Vn,switchDocUrl:()=>zn});function Vn(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=le(e.content);if(t)return[{kind:\"show-map\",content:t}];let r=[{kind:\"leave-map\"}];return e.file_url&&e.file_url!==n.docUrl?(r.push({kind:\"switch-doc\",url:e.file_url,name:e.file_name||void 0,title:e.doc_title||void 0,updatedAt:e.updated_at||void 0}),r):(e.current_page&&r.push({kind:\"show-page\",page:e.current_page}),r)}function zn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return je(Fn);})();", SOURCE_HASH: "6388a573408bb1f8" };
|
|
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: "5bacfcb0f5b69825" };
|
package/server/handler.js
CHANGED
|
@@ -77,7 +77,66 @@ const originOf = (u) => { try { return new URL(u).origin; } catch { return ""; }
|
|
|
77
77
|
//
|
|
78
78
|
// Signalé par un audit externe. Fermer le chemin d'abord, migrer ensuite — dans cet ordre.
|
|
79
79
|
const PDFJS = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174";
|
|
80
|
-
|
|
80
|
+
// ⚠️ VERSION EXACTE, PAS `@2` (constat P2-4). L'étiquette `@2` de jsdelivr suit la dernière 2.x :
|
|
81
|
+
// la page servait donc, aux visiteurs, le code que Supabase avait publié le matin même — sans que
|
|
82
|
+
// personne n'ait rien déployé, ni relu, ni pu revenir en arrière. Le jour où elle résolvait vers
|
|
83
|
+
// 2.112.3, un correctif de la veille aurait changé ce qui tourne dans le navigateur d'un client
|
|
84
|
+
// pendant une présentation en cours. Une dépendance mouvante n'est pas une dépendance, c'est un
|
|
85
|
+
// abonnement à ce que décide un tiers.
|
|
86
|
+
const SUPAJS = "https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2.112.3/dist/umd/supabase.js";
|
|
87
|
+
const LEAFLET = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js";
|
|
88
|
+
// ⚠️ `weekly` EST UN CANAL, PAS UNE VERSION — le même défaut que l'étiquette `@2` de jsdelivr :
|
|
89
|
+
// le code de cartographie exécuté chez le visiteur changeait chaque semaine sans qu'aucun
|
|
90
|
+
// déploiement le décide. Google ne publie pas d'empreinte et son chargeur injecte d'autres
|
|
91
|
+
// scripts, donc l'intégrité restera hors de portée ici ; épingler la version trimestrielle rend
|
|
92
|
+
// au moins le changement DÉCIDÉ, et daté. À relever à chaque montée — Google retire les versions
|
|
93
|
+
// après environ un an.
|
|
94
|
+
const MAPS_VERSION = "3.58";
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Empreintes des dépendances tierces (constat P2-4).
|
|
98
|
+
*
|
|
99
|
+
* ⚠️ CE QUI EST EN JEU : un script tiers entre dans la page avec EXACTEMENT les droits de notre
|
|
100
|
+
* code — même origine, même session, même accès au document affiché. Une version épinglée dit
|
|
101
|
+
* seulement quel fichier on demande ; elle ne dit rien de ce qu'on reçoit. L'empreinte, elle, fait
|
|
102
|
+
* refuser le navigateur si un octet a changé, quelle que soit la raison : CDN compromis, compte de
|
|
103
|
+
* publication détourné, ou intermédiaire.
|
|
104
|
+
*
|
|
105
|
+
* ⚠️ ET ELLE OBLIGE À ÉPINGLER. `integrity` sur une URL mouvante casserait la page à la première
|
|
106
|
+
* publication du tiers : les deux vont ensemble, l'un n'a pas de sens sans l'autre.
|
|
107
|
+
*
|
|
108
|
+
* Relevées sur les octets réellement servis, pas recopiées d'une documentation. Pour les vérifier
|
|
109
|
+
* ou les mettre à jour après une montée de version :
|
|
110
|
+
*
|
|
111
|
+
* curl -sL <url> | openssl dgst -sha384 -binary | openssl base64 -A
|
|
112
|
+
*
|
|
113
|
+
* Le banc navigateur les reconfronte à ce que les CDN servent — deux exemplaires d'un même fait
|
|
114
|
+
* doivent être comparés par quelqu'un au moins une fois.
|
|
115
|
+
*/
|
|
116
|
+
const TIERS = {
|
|
117
|
+
pdf: { url: PDFJS + "/pdf.min.js", sri: "sha384-/1qUCSGwTur9vjf/z9lmu/eCUYbpOTgSjmpbMQZ1/CtX2v/WcAIKqRv+U1DUCG6e" },
|
|
118
|
+
// ⚠️ Le worker N'A PAS DE BALISE, donc pas d'attribut `integrity` : il est chargé par pdf.js,
|
|
119
|
+
// pas par le document. Mais ses octets passent déjà par notre code (ils sont récupérés en texte
|
|
120
|
+
// puis transformés en blob de même origine, sans quoi le navigateur refuserait un worker
|
|
121
|
+
// distant) — on les vérifie donc à la main, là où ils passent. Sans ça, l'empreinte de
|
|
122
|
+
// `pdf.min.js` protégerait la petite moitié et laisserait la grande (1 Mo contre 300 ko) entrer
|
|
123
|
+
// sans contrôle, alors même qu'elle voit tout le contenu du document.
|
|
124
|
+
pdfWorker: { url: PDFJS + "/pdf.worker.min.js", sri: "sha384-SnzOobpRMLXZ52iJvZm/C0fYw0OQemTXzTjIsdsfMcrCtCEe9qgzxTd3RSklO5x2" },
|
|
125
|
+
supa: { url: SUPAJS, sri: "sha384-qafw21c/iciq0VXsi9FzkfoQv5I/V0iqE4lSNcKXPnW9/UTJLnv5CcN4FHxVLnKg" },
|
|
126
|
+
leaflet: { url: LEAFLET, sri: "sha384-cxOPjt7s7Iz04uaHJceBmS+qpjv2JkIHNVcuOrM+YHwZOmJGBXI00mdUXEq65HTH" },
|
|
127
|
+
// ⚠️ UNE FEUILLE DE STYLE TIERCE N'EST PAS INOFFENSIVE, et celle-ci n'était comptée nulle part.
|
|
128
|
+
// Du CSS peut déplacer, agrandir et rendre transparent n'importe quel élément : un bouton qu'on
|
|
129
|
+
// croit cliquer n'est pas forcément celui qu'on clique. Même origine, même empreinte, même règle.
|
|
130
|
+
leafletCss: { url: "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css", sri: "sha384-sHL9NAb7lN7rfvG5lfHpm643Xkcjzp4jFvuavGOndn6pjVqS6ny56CAt3nsEVT4H" },
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// ⚠️ `crossorigin` EST OBLIGATOIRE avec `integrity` sur une ressource d'une autre origine : sans
|
|
134
|
+
// lui, la réponse est opaque, le navigateur ne peut pas la lire pour la hacher, et il refuse le
|
|
135
|
+
// script — silencieusement du point de vue de la page. L'oublier ne dégrade pas la protection,
|
|
136
|
+
// ça casse la page. Les deux ne se séparent jamais : d'où cette fonction, plutôt que trois
|
|
137
|
+
// balises écrites à la main dont l'une finirait par en perdre un.
|
|
138
|
+
const balise = (nonce, tiers) =>
|
|
139
|
+
`<script nonce="${nonce}" src="${tiers.url}" integrity="${tiers.sri}" crossorigin="anonymous"></script>`;
|
|
81
140
|
|
|
82
141
|
// ————— Couche LIVE partagée (présence + chat historisé) — présentateur ET audience —————
|
|
83
142
|
// CSS injecté dans les deux vues.
|
|
@@ -360,7 +419,7 @@ var Live=(function(){
|
|
|
360
419
|
if(y)y.onclick=function(){close();if(onOk)onOk();};if(n)n.onclick=close;m.onclick=function(e){if(e.target===m)close();};
|
|
361
420
|
document.addEventListener('keydown',key);m.classList.add('open');if(y)try{y.focus();}catch(e){}}
|
|
362
421
|
function history(){fetch('/api/doc?present='+encodeURIComponent(SLUG)+'&chat=1').then(function(r){return r.json();}).then(function(d){var box=document.getElementById('chatMsgs');if(d&&d.messages&&d.messages.length){d.messages.forEach(function(m){addMsg(m);});}else if(box&&!box.children.length){box.innerHTML='<div class=chat-empty>Aucun message. Lancez la discussion.</div>';}if(d&&typeof d.locked!=='undefined')applyLock(d.locked);_histDone=true;}).catch(function(){_histDone=true;});}
|
|
363
|
-
function react(id,e){if(!ME||!id||!e)return;fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'present-react',slug:SLUG,msgId:+id,emoji:e,reactor:
|
|
422
|
+
function react(id,e){if(!ME||!id||!e)return;fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'present-react',slug:SLUG,msgId:+id,emoji:e,reactor:MOIREF})}).then(majDiffusee).catch(function(){});}
|
|
364
423
|
function setReply(id){var m=msgData[id];if(!m||m.deleted)return;var nm=m.author_name||'Invité';replyCtx={id:+id,name:nm,text:(m.body||'').slice(0,120)};var el=document.getElementById('chatReply');if(el){el.style.display='flex';el.innerHTML='<span class=cq><b>'+esc(nm)+'</b> '+esc((m.body||'').slice(0,80))+'</span><button id=chatReplyX title=Annuler>×</button>';var x=document.getElementById('chatReplyX');if(x)x.addEventListener('click',clearReply);}var t=document.getElementById('chatText');if(t)t.focus();}
|
|
365
424
|
function clearReply(){replyCtx=null;var el=document.getElementById('chatReply');if(el){el.style.display='none';el.innerHTML='';}}
|
|
366
425
|
function send(){var i=document.getElementById('chatText');var t=(i.value||'').trim();if(!t||!ME)return;if(LOCKED&&!canMod())return;i.value='';toggleSend();
|
|
@@ -438,7 +497,27 @@ var Live=(function(){
|
|
|
438
497
|
if(pick&&!pick._w){pick._w=1;pick.innerHTML=EMOJIS.map(function(e){return '<button data-e="'+e+'">'+e+'</button>';}).join('');
|
|
439
498
|
pick.addEventListener('click',function(e){e.stopPropagation();var b=e.target.closest?e.target.closest('button'):null;if(b){react(pick.__id,b.getAttribute('data-e'));pick.classList.remove('open');}});
|
|
440
499
|
document.addEventListener('click',function(){pick.classList.remove('open');});}}
|
|
441
|
-
|
|
500
|
+
// ⚠️ NOTRE IDENTITÉ PUBLIQUE, ET LA SEULE. Elle remplace l'adresse partout où une identité
|
|
501
|
+
// devait sortir : clé du canal de présence, charge de présence, identité d'un réacteur,
|
|
502
|
+
// appartenance d'un message. Dérivée du jeton d'auteur — celui qui autorise déjà à modifier et
|
|
503
|
+
// supprimer — donc « c'est moi » dit enfin la même chose que « j'ai le droit ».
|
|
504
|
+
//
|
|
505
|
+
// ⚠️ PRÉPARÉE AU CHARGEMENT, PAS DANS LA CONNEXION. Premier jet : la connexion attendait le
|
|
506
|
+
// hachage avant de faire quoi que ce soit — tout retardé pour un calcul local, et deux essais
|
|
507
|
+
// tombés qui pilotent la page de façon synchrone. Le hachage ne dépend que du jeton d'auteur :
|
|
508
|
+
// il n'a aucune raison d'attendre un appel, et la souscription réseau lui laisse tout le temps.
|
|
509
|
+
//
|
|
510
|
+
// La CLÉ de présence, elle, n'en a jamais eu besoin : MYID est déjà tiré au sort par navigateur.
|
|
511
|
+
// Y mettre l'adresse ne servait qu'au confort de lecture, au prix d'une identité publiée à tous.
|
|
512
|
+
//
|
|
513
|
+
// ⚠️ ET PAS D'ACCENT GRAVE DANS CE COMMENTAIRE : il vit DANS le gabarit de la page, donc un
|
|
514
|
+
// accent grave y ferme la chaîne. Ce bloc l'a appris à ses dépens il y a trois minutes.
|
|
515
|
+
//
|
|
516
|
+
// Vide dans un contexte non sécurisé (pas de crypto.subtle) : on s'annonce alors sans identité
|
|
517
|
+
// plutôt qu'avec une fausse, et les boutons qui supposent la propriété disparaissent.
|
|
518
|
+
var MOIREF='';
|
|
519
|
+
var REFPRETE=(function(){ try{ return Player.live.referenceAuteur(authToken()).then(function(r){ MOIREF=r||''; }).catch(function(){ MOIREF=''; }); }catch(e){ return Promise.resolve(); } })();
|
|
520
|
+
function connect(slug,me,control){if(!window.supabase||!LIVECFG.supaUrl||!LIVECFG.supaKey||!slug)return;SLUG=slug;ME=me;CONTROL=control||null;try{ME.ref=MOIREF;}catch(e){}
|
|
442
521
|
var pb=document.getElementById('presBtn');if(pb)pb.style.display='inline-flex';
|
|
443
522
|
var cb=document.getElementById('chatBtn');if(cb)cb.style.display='inline-flex';var _fb=document.getElementById('chatFab');if(_fb)_fb.classList.add('on');
|
|
444
523
|
wire();history();
|
|
@@ -449,7 +528,7 @@ var Live=(function(){
|
|
|
449
528
|
// Chez nous les deux cles different deja, mais par heureux hasard : le declarer rend
|
|
450
529
|
// intentionnel ce qui n'etait qu'une consequence, et une topologie peut changer.
|
|
451
530
|
auth:{storageKey:LIVECFG.liveAuthKey||'dmp-live-auth',persistSession:true,autoRefreshToken:true}});
|
|
452
|
-
ch=sb.channel('plive-'+slug,{config:{presence:{key:
|
|
531
|
+
ch=sb.channel('plive-'+slug,{config:{presence:{key:MYID}}});
|
|
453
532
|
ch.on('presence',{event:'sync'},function(){renderPres(ch.presenceState());});
|
|
454
533
|
// Plus d'abonnement à la table des messages : elle n'est plus publiée ni lisible
|
|
455
534
|
// publiquement. Tout passe par la diffusion, et l'historique par la route de chat.
|
|
@@ -576,7 +655,7 @@ var Live=(function(){
|
|
|
576
655
|
// par accident : le jour où quelqu'un rebranche un paramètre, la charge d'un canal public
|
|
577
656
|
// redeviendrait crue sans que rien ne le signale. On coupe le chemin, pas seulement l'usage.
|
|
578
657
|
ch.on('broadcast',{event:'map'},function(){if(_onMap)_onMap();});
|
|
579
|
-
ch.subscribe(function(st){if(st==='SUBSCRIBED'){ch.track({name:me.name,
|
|
658
|
+
ch.subscribe(function(st){if(st==='SUBSCRIBED'){REFPRETE.then(function(){ME.ref=MOIREF;ch.track({name:me.name,ref:MOIREF,avatar:me.avatar,role:me.role,member:!!me.member,uid:presId()});sendAttend();});}});
|
|
580
659
|
_tyIv=setInterval(renderTyping,1500);
|
|
581
660
|
_atIv=setInterval(sendAttend,25000);
|
|
582
661
|
// Filet de sécurité : au déchargement de la page/iframe (fermeture, reload, switch), on retire la présence
|
|
@@ -624,12 +703,12 @@ var Map3DD=(function(){
|
|
|
624
703
|
var useG=!!GMAPS_KEY; // carte de base = Google Maps si une clé est fournie, sinon repli OpenStreetMap (Leaflet)
|
|
625
704
|
function loadLeaflet(cb){ if(window.L){cb();return;} var iv=setInterval(function(){if(window.L){clearInterval(iv);cb();}},80);
|
|
626
705
|
if(leafletLoading)return; leafletLoading=true;
|
|
627
|
-
var css=document.createElement('link');css.rel='stylesheet';css.href='
|
|
628
|
-
var s=document.createElement('script');s.src='
|
|
706
|
+
var css=document.createElement('link');css.rel='stylesheet';css.href='${TIERS.leafletCss.url}';css.integrity='${TIERS.leafletCss.sri}';css.crossOrigin='anonymous';document.head.appendChild(css);
|
|
707
|
+
var s=document.createElement('script');s.src='${TIERS.leaflet.url}';s.integrity='${TIERS.leaflet.sri}';s.crossOrigin='anonymous';document.body.appendChild(s); }
|
|
629
708
|
// Google Maps JS chargé à la demande, seulement si une clé est fournie (GMAPS_KEY).
|
|
630
709
|
function loadGoogle(cb){ if(window.google&&window.google.maps){cb();return;} if(!GMAPS_KEY)return; var iv=setInterval(function(){if(window.google&&window.google.maps){clearInterval(iv);cb();}},120);
|
|
631
710
|
if(gLoading)return; gLoading=true;
|
|
632
|
-
var s=document.createElement('script');s.async=true;s.src='https://maps.googleapis.com/maps/api/js?key='+encodeURIComponent(GMAPS_KEY)+'&v
|
|
711
|
+
var s=document.createElement('script');s.async=true;s.src='https://maps.googleapis.com/maps/api/js?key='+encodeURIComponent(GMAPS_KEY)+'&v=${MAPS_VERSION}&loading=async';document.body.appendChild(s); }
|
|
633
712
|
function loadBase(cb){ if(useG)loadGoogle(cb); else loadLeaflet(cb); }
|
|
634
713
|
function ensureMap(center,zoom){ if(map)return;
|
|
635
714
|
if(useG){ var el=document.getElementById('map3dd'); if(!el||!window.google)return;
|
|
@@ -1547,7 +1626,15 @@ function sendHtml(res, status, html, scriptSrc, imgExtra, frameAncestors) {
|
|
|
1547
1626
|
// personne n'a franchie serait un mauvais échange. `'self'` ferme l'exfiltration, qui est le
|
|
1548
1627
|
// risque réel.
|
|
1549
1628
|
"form-action 'self'",
|
|
1550
|
-
//
|
|
1629
|
+
// ⚠️ CE COMMENTAIRE ANNONÇAIT `'none'` POUR LA PAGE PUBLIQUE, ET C'ÉTAIT FAUX. Un lien tracé
|
|
1630
|
+
// sans `embed` est servi en `frame-ancestors 'self'` (cf. la route `doc`) : encadrement de
|
|
1631
|
+
// MÊME ORIGINE uniquement, ce qui reste sain — un détournement de clic suppose une page
|
|
1632
|
+
// étrangère, et une page hostile sur notre propre origine serait déjà une compromission bien
|
|
1633
|
+
// pire. Mais la phrase, elle, faisait croire à un refus total.
|
|
1634
|
+
//
|
|
1635
|
+
// Trouvé en écrivant l'essai de bout en bout : l'assertion recopiait ce commentaire, et c'est
|
|
1636
|
+
// elle qui est tombée. Le comportement est désormais VÉRIFIÉ par ce banc, donc ce que vous
|
|
1637
|
+
// lisez ici ne peut plus dériver seul.
|
|
1551
1638
|
`frame-ancestors ${frameAncestors || "'none'"}`,
|
|
1552
1639
|
].join("; "));
|
|
1553
1640
|
res.end(html);
|
|
@@ -1997,8 +2084,8 @@ ${LEGAL_CSS}
|
|
|
1997
2084
|
${legalFooter({ tracked: !preview && !!share.slug, sansExpediteur: !share.recipient_email && !share.created_by })}
|
|
1998
2085
|
${brandLogo || !brandIntroRuntime ? "" : `<script nonce="${nonce}">(${brandIntroRuntime.toString()})();</script>`}
|
|
1999
2086
|
<script nonce="${nonce}">${PLAYER_BROWSER_JS}</script>
|
|
2000
|
-
|
|
2001
|
-
${preview ?
|
|
2087
|
+
${balise(nonce, TIERS.pdf)}
|
|
2088
|
+
${preview ? `${balise(nonce, TIERS.supa)}
|
|
2002
2089
|
<script nonce="${nonce}">var LIVECFG={supaUrl:${JSON.stringify(share.supa_url || "")},supaKey:${JSON.stringify(share.supa_key || "")},hostAuthKey:${JSON.stringify(cleSessionHote())},liveAuthKey:${JSON.stringify(CLE_SESSION_PLAYER)},guestKey:${JSON.stringify(CLE_INVITE)}};var GMAPS_KEY=${JSON.stringify((PLAYER.config && PLAYER.config.mapsKey) || "" || "")};${LIVE_JS}
|
|
2003
2090
|
${MAP_JS}</script>` : ""}
|
|
2004
2091
|
<script nonce="${nonce}">
|
|
@@ -2287,14 +2374,28 @@ ${LEGAL_CSS}
|
|
|
2287
2374
|
render();
|
|
2288
2375
|
}
|
|
2289
2376
|
function setZoom(z){ zoom=Player.viewer.clampZoom(z); document.getElementById('zlbl').textContent=Math.round(zoom*100)+'%'; if(pdfDoc) build(); }
|
|
2290
|
-
//
|
|
2377
|
+
// ⚠️ UN REFUS ARRÊTE LE LECTEUR, et les deux replis plus doux ont été MESURÉS avant d'être
|
|
2378
|
+
// écartés — pas jugés sur leur mine :
|
|
2379
|
+
//
|
|
2380
|
+
// • rendre l'URL distante : pdf.js l'enveloppe LUI-MÊME dans un blob de même origine, et le
|
|
2381
|
+
// code non vérifié s'exécute. C'était le comportement livré : l'empreinte ne servait à rien ;
|
|
2382
|
+
// • laisser la valeur vide : pdf.js DÉDUIT une adresse par défaut depuis sa propre position
|
|
2383
|
+
// sur le CDN, et le charge quand même.
|
|
2384
|
+
//
|
|
2385
|
+
// Les deux annulaient la vérification EN SILENCE, et aucun raisonnement ne l'aurait dit : il a
|
|
2386
|
+
// fallu regarder les workers réellement créés. Il ne reste qu'une sortie honnête — ne pas
|
|
2387
|
+
// afficher, et le dire. Un document qu'on ne rend pas se voit ; un document rendu par du code
|
|
2388
|
+
// qu'on n'a pas vérifié, non.
|
|
2389
|
+
//
|
|
2390
|
+
// Le drapeau posé sur la fenêtre permet au banc d'EXIGER le refus, au lieu de constater une
|
|
2391
|
+
// absence de rendu qui aurait dix causes.
|
|
2291
2392
|
var wsrc=CFG.pdfjs+'/pdf.worker.min.js';
|
|
2292
2393
|
try{
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
start();
|
|
2296
|
-
}).catch(function(){
|
|
2297
|
-
}catch(e){
|
|
2394
|
+
Player.viewer.workerBlobUrl(wsrc,'${TIERS.pdfWorker.sri}').then(function(u){
|
|
2395
|
+
if(!u){ window.__workerRefuse=1; loadError("Document non affiche : une dependance n a pas pu etre verifiee."); return; }
|
|
2396
|
+
pdfjsLib.GlobalWorkerOptions.workerSrc=u; start();
|
|
2397
|
+
}).catch(function(){ window.__workerRefuse=1; loadError("Document non affiche : une dependance n a pas pu etre verifiee."); });
|
|
2398
|
+
}catch(e){ window.__workerRefuse=1; loadError("Document non affiche : une dependance n a pas pu etre verifiee."); }
|
|
2298
2399
|
// Bord à bord en une-page mobile : chaque millimètre compte, surtout pour un PDF paysage.
|
|
2299
2400
|
function baseWidth(){ var pad=(onePage&&window.innerWidth<=820)?0:30; return (scrollEl.clientWidth||900)-pad; }
|
|
2300
2401
|
// Hauteur du document OCCULTÉE par la bottom sheet du bot (mobile, état compagnon) : la page une-page se
|
|
@@ -2577,8 +2678,8 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
|
|
|
2577
2678
|
${PLAYER.branding.poweredBy ? `<div class=brand>Propulsé par ${esc(PLAYER.branding.poweredBy)}</div>` : ""}
|
|
2578
2679
|
${legalFooter({ tracked: true })}
|
|
2579
2680
|
<script nonce="${nonce}">${PLAYER_BROWSER_JS}</script>
|
|
2580
|
-
|
|
2581
|
-
|
|
2681
|
+
${balise(nonce, TIERS.pdf)}
|
|
2682
|
+
${balise(nonce, TIERS.supa)}
|
|
2582
2683
|
<script nonce="${nonce}">
|
|
2583
2684
|
(function(){
|
|
2584
2685
|
var CFG=${cfg.replace(/</g, "\\u003c")};
|
|
@@ -2603,10 +2704,10 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
|
|
|
2603
2704
|
}
|
|
2604
2705
|
var wsrc=CFG.pdfjs+'/pdf.worker.min.js';
|
|
2605
2706
|
function boot(){
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
load();
|
|
2609
|
-
}).catch(function(){
|
|
2707
|
+
Player.viewer.workerBlobUrl(wsrc,'${TIERS.pdfWorker.sri}').then(function(u){
|
|
2708
|
+
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; }
|
|
2709
|
+
pdfjsLib.GlobalWorkerOptions.workerSrc=u; load();
|
|
2710
|
+
}).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."; });
|
|
2610
2711
|
}
|
|
2611
2712
|
function load(){
|
|
2612
2713
|
pdfjsLib.getDocument({url:CFG.fileUrl,isEvalSupported:false}).promise.then(function(pdf){
|
|
@@ -3847,6 +3948,9 @@ async function handler(req, res) {
|
|
|
3847
3948
|
}
|
|
3848
3949
|
}
|
|
3849
3950
|
|
|
3850
|
-
|
|
3951
|
+
// ⚠️ `TIERS` est exporté pour être CONFRONTÉ, pas pour être utilisé. Le banc navigateur et la
|
|
3952
|
+
// forge doivent pouvoir demander « quelles dépendances tierces, à quelles empreintes » sans en
|
|
3953
|
+
// tenir une seconde liste — c'est la seule façon qu'une empreinte périmée finisse par se voir.
|
|
3954
|
+
module.exports = { handler, init, TIERS };
|
|
3851
3955
|
|
|
3852
3956
|
// redeploy: forcer le build production (Vercel a sauté la prod du merge #463 — wording re-partage).
|
package/server/presentations.js
CHANGED
|
@@ -74,6 +74,35 @@ async function touchPresentation(slug, control) {
|
|
|
74
74
|
// Liste des présentations en cours (membre authentifié). Auto-purge : une présentation active dont le
|
|
75
75
|
// dernier heartbeat remonte à > STALE_MS (présentateur parti sans clôturer) est marquée inactive.
|
|
76
76
|
const STALE_MS = 3 * 60 * 1000;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* ÉCRIRE SEULEMENT SI LA CONDITION TIENT ENCORE — au moment de l'écriture, pas au moment du contrôle.
|
|
80
|
+
*
|
|
81
|
+
* ⚠️ LE DÉFAUT QUE ÇA FERME. Toutes les écritures de pilotage faisaient : lire la ligne, vérifier le
|
|
82
|
+
* jeton, PATCHER. Entre la vérification et le PATCH, la présentation peut avoir été TERMINÉE — et
|
|
83
|
+
* comme le pilotage écrit « active: true », la requête retardée la ROUVRAIT pour toute l'audience.
|
|
84
|
+
* Le présentateur avait cliqué « Terminer », vu l'écran de fin, et l'audience continuait de suivre.
|
|
85
|
+
* Un second SELECT juste avant le PATCH ne changerait rien : la fenêtre se déplace, elle ne ferme pas.
|
|
86
|
+
*
|
|
87
|
+
* ⚠️ ET LA CONDITION N'EST PAS « active = true », que l'audit prescrivait. Elle casserait un
|
|
88
|
+
* comportement voulu : une présentation devient inactive au bout de trois minutes sans battement
|
|
89
|
+
* (le portable du présentateur a dormi), et sa page suivante DOIT la remettre en ligne — un
|
|
90
|
+
* présentateur anonyme n'a aucun autre moyen de revenir. La bonne condition est celle qui sépare
|
|
91
|
+
* la fin DÉCIDÉE de la péremption CONSTATÉE : terminer ANNULE le jeton de contrôle. On filtre donc
|
|
92
|
+
* sur le jeton, et chaque chemin porte dans sa condition le critère qu'il vérifiait déjà.
|
|
93
|
+
*
|
|
94
|
+
* Zéro ligne touchée = refus. PostgREST le dit en rendant un tableau vide, à condition de demander
|
|
95
|
+
* la représentation — sans quoi on ne saurait pas distinguer « rien à faire » de « rien fait ».
|
|
96
|
+
*/
|
|
97
|
+
async function ecrireSiEncoreVrai(condition, corps) {
|
|
98
|
+
const lignes = await PLAYER.db.request(condition, {
|
|
99
|
+
method: "PATCH", headers: { Prefer: "return=representation" }, body: corps,
|
|
100
|
+
});
|
|
101
|
+
return Array.isArray(lignes) && lignes.length > 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Le rang, seulement là où la colonne existe — sinon PostgREST rejette le PATCH ENTIER. */
|
|
105
|
+
const filtreRang = (rang) => (rang && rang.controle ? `&write_seq=lt.${rang.rang}` : "");
|
|
77
106
|
async function listActivePresentations(email) {
|
|
78
107
|
const me = lc(email);
|
|
79
108
|
const rows = await PLAYER.db.request("doc_presentations?active=eq.true&select=slug,doc_id,file_url,file_name,doc_title,presenter_name,owner_email,owner_name,owner_avatar,current_page,last_seen,created_at,updated_at&order=updated_at.desc&limit=100");
|
|
@@ -193,8 +222,16 @@ async function setPage(slug, control, page, seq) {
|
|
|
193
222
|
// navigateur n'affiche rien — la page qu'il voulait écrire est déjà dépassée par une plus récente.
|
|
194
223
|
if (rang.perime) return { ok: true, perime: true };
|
|
195
224
|
const p = Math.max(1, Math.trunc(Number(page) || 1));
|
|
196
|
-
|
|
197
|
-
|
|
225
|
+
// Le jeton DANS la condition : si la présentation a été terminée entre-temps, il a été annulé
|
|
226
|
+
// et cette écriture ne trouve plus sa ligne. Le rang aussi, pour que deux écritures concurrentes
|
|
227
|
+
// ne puissent pas s'inverser — la comparaison et l'écriture deviennent le même geste.
|
|
228
|
+
const ecrit = await ecrireSiEncoreVrai(
|
|
229
|
+
`doc_presentations?slug=eq.${enc(slug)}&control_hash=eq.${sha(control)}${filtreRang(rang)}`,
|
|
230
|
+
{ current_page: p, active: true, last_seen: new Date().toISOString(), updated_at: new Date().toISOString(), ...(rang.controle ? { write_seq: rang.rang } : {}) },
|
|
231
|
+
);
|
|
232
|
+
// Refus SILENCIEUX, comme un rang périmé : le navigateur n'a rien à afficher, la page qu'il
|
|
233
|
+
// voulait écrire n'a simplement plus de présentation où aller.
|
|
234
|
+
return ecrit ? { ok: true } : { ok: true, perime: true };
|
|
198
235
|
}
|
|
199
236
|
|
|
200
237
|
// Fin de présentation → l'audience voit « terminée ».
|
|
@@ -202,7 +239,17 @@ async function endPresentation(slug, control) {
|
|
|
202
239
|
const row = await getPresentation(slug);
|
|
203
240
|
if (!row) return { ok: false, status: 404 };
|
|
204
241
|
if (!tokenMatches(control, row.control_hash)) return { ok: false, status: 403 };
|
|
205
|
-
|
|
242
|
+
// ⚠️ CONDITIONNÉ AU JETON LUI AUSSI, et pas seulement par symétrie : entre la vérification et
|
|
243
|
+
// l'écriture, le propriétaire a pu REPRENDRE la main depuis un autre appareil, ce qui régénère
|
|
244
|
+
// le jeton. Une fin retardée émise avec l'ancien fermerait alors la session neuve — le contraire
|
|
245
|
+
// de ce que la reprise vient d'obtenir.
|
|
246
|
+
await ecrireSiEncoreVrai(
|
|
247
|
+
`doc_presentations?slug=eq.${enc(slug)}&control_hash=eq.${sha(control)}`,
|
|
248
|
+
{ ...CLOTURE, updated_at: new Date().toISOString() },
|
|
249
|
+
);
|
|
250
|
+
// Terminer est IDEMPOTENT : ne pas trouver de ligne veut dire « déjà terminée, ou reprise
|
|
251
|
+
// ailleurs ». Dans les deux cas l'appelant n'a plus rien à faire, et un échec l'inviterait à
|
|
252
|
+
// réessayer une action qui n'a plus d'objet.
|
|
206
253
|
return { ok: true };
|
|
207
254
|
}
|
|
208
255
|
|
|
@@ -210,7 +257,42 @@ async function endPresentation(slug, control) {
|
|
|
210
257
|
// Pièce jointe : URL d'upload SIGNÉE (service role) → le client PUT directement dans le bucket. Type/taille
|
|
211
258
|
// validés par le bucket (image/*+pdf, ≤10 Mo). L'URL publique finale est renvoyée pour attacher au message.
|
|
212
259
|
const ATT_KINDS = { "image/png": "image", "image/jpeg": "image", "image/webp": "image", "image/gif": "image", "application/pdf": "pdf" };
|
|
260
|
+
/**
|
|
261
|
+
* UNE PRÉSENTATION TERMINÉE DEVIENT UNE ARCHIVE : on la relit, on ne l'écrit plus.
|
|
262
|
+
*
|
|
263
|
+
* ⚠️ « TERMINÉE » VEUT DIRE DÉCIDÉE, PAS PÉRIMÉE — la distinction qui traverse tout ce module.
|
|
264
|
+
* Une présentation devient inactive après trois minutes sans battement, et son présentateur doit
|
|
265
|
+
* pouvoir revenir ; refuser le chat dans ce cas couperait la parole à une salle pendant que
|
|
266
|
+
* l'orateur rebranche son portable. Terminer, en revanche, ANNULE le jeton de contrôle : c'est
|
|
267
|
+
* cette trace-là qui distingue une fin d'une somnolence, et c'est elle qu'on lit.
|
|
268
|
+
*
|
|
269
|
+
* ⚠️ LA LECTURE RESTE OUVERTE, et c'est un choix de produit, pas un oubli. Ce qui s'est dit
|
|
270
|
+
* pendant une présentation a de la valeur après : on relit une question, on retrouve un lien.
|
|
271
|
+
* Le risque n'est pas dans la lecture, il est dans l'écriture — un message ajouté après coup dans
|
|
272
|
+
* un fil que plus personne ne surveille, une pièce jointe déposée dans le bucket d'une session
|
|
273
|
+
* close. On ferme donc les sept portes qui ÉCRIVENT, et aucune de celles qui lisent.
|
|
274
|
+
*/
|
|
275
|
+
async function estArchive(slug) {
|
|
276
|
+
const row = await getPresentation(slug);
|
|
277
|
+
// Pas de présentation du tout : ce n'est pas une archive, et l'appelant a ses propres 404.
|
|
278
|
+
if (!row) return false;
|
|
279
|
+
// ⚠️ LES DEUX FAITS, PAS UN SEUL — et le premier jet ne prenait que le jeton annulé. Une ligne
|
|
280
|
+
// dont la colonne n'est simplement pas remontée aurait alors été prise pour une session close :
|
|
281
|
+
// un champ ABSENT et un champ NUL sont la même chose une fois passés par JSON, et cette
|
|
282
|
+
// confusion aurait fermé le chat de présentations parfaitement vivantes. Six essais l'ont dit
|
|
283
|
+
// tout de suite, ce qui est exactement leur métier.
|
|
284
|
+
//
|
|
285
|
+
// Terminer écrit les deux ensemble (active: false ET jeton annulé) ; une péremption n'écrit que
|
|
286
|
+
// le premier. Exiger les deux distingue donc la fin décidée de la somnolence, sans dépendre
|
|
287
|
+
// d'une absence.
|
|
288
|
+
return row.active === false && row.control_hash == null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Refus commun, pour que les sept routes répondent la même chose. */
|
|
292
|
+
const REFUS_ARCHIVE = { ok: false, status: 409, error: "ended" };
|
|
293
|
+
|
|
213
294
|
async function createUploadUrl(slug, name, type) {
|
|
295
|
+
if (await estArchive(slug)) return REFUS_ARCHIVE;
|
|
214
296
|
|
|
215
297
|
// ⚠️ `Object.hasOwn` et pas une simple lecture : un objet littéral hérite de `constructor`,
|
|
216
298
|
// `toString`, `valueOf`, `__proto__`… `ATT_KINDS["constructor"]` rend une FONCTION, donc une
|
|
@@ -246,16 +328,59 @@ async function createUploadUrl(slug, name, type) {
|
|
|
246
328
|
* ailleurs. C'est le même jeu de champs que celui servi à l'historique.
|
|
247
329
|
*/
|
|
248
330
|
const CHAMPS_PUBLICS = [
|
|
249
|
-
"id", "author_name", "
|
|
331
|
+
"id", "author_name", "author_avatar", "is_presenter", "is_member",
|
|
250
332
|
"body", "attachment", "reactions", "reply_to", "reply_name", "reply_text",
|
|
251
333
|
"deleted", "edited", "created_at",
|
|
252
334
|
];
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* IDENTITÉ PUBLIQUE D'UN AUTEUR — opaque, stable, et dérivée de ce qui AUTORISE déjà.
|
|
338
|
+
*
|
|
339
|
+
* ⚠️ `author_email` ÉTAIT DANS CETTE LISTE. Chaque message rendu à n'importe quel participant
|
|
340
|
+
* portait donc l'adresse de son auteur, et nos audiences sont des visiteurs anonymes externes :
|
|
341
|
+
* il suffisait d'ouvrir l'historique du chat pour repartir avec les adresses de toute l'équipe.
|
|
342
|
+
*
|
|
343
|
+
* ⚠️ CE QUI REMPLACE N'EST PAS UN PSEUDONYME TIRÉ AU SORT, mais l'empreinte du JETON D'AUTEUR —
|
|
344
|
+
* celui-là même qui autorise à modifier et supprimer. Deux conséquences, toutes deux voulues :
|
|
345
|
+
*
|
|
346
|
+
* • aucun secret d'instance n'est nécessaire. Hacher une adresse sans sel ne protège rien (le
|
|
347
|
+
* domaine est connu, les prénoms se devinent) ; ici l'entrée est un jeton tiré au sort ;
|
|
348
|
+
* • « c'est mon message » DIT ENFIN LA MÊME CHOSE QUE « j'ai le droit d'y toucher ». L'ancien
|
|
349
|
+
* `isMine` comparait les adresses alors que l'édition n'a jamais regardé que le jeton : un
|
|
350
|
+
* membre sur un second navigateur voyait un bouton « Modifier » qui lui répondait 403.
|
|
351
|
+
*
|
|
352
|
+
* Un tour de hachage de plus que la valeur stockée : ce qui sort n'est jamais le matériel gardé
|
|
353
|
+
* en base, même si le connaître ne suffirait pas à usurper.
|
|
354
|
+
*/
|
|
355
|
+
const refAuteur = (hash) => (hash ? sha("ref:" + hash).slice(0, 16) : null);
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* ⚠️ LES ANCIENNES RÉACTIONS PORTENT DES ADRESSES, et aucune migration ne les réécrira : le
|
|
359
|
+
* client stockait `email || nom` comme identité de réacteur. On garde le COMPTE — la pastille
|
|
360
|
+
* affiche toujours « 👍 3 » — en remplaçant chaque identité illisible par un jeton de place qui
|
|
361
|
+
* ne correspond à personne. Un participant ne se reconnaît plus dans ses vieilles réactions ;
|
|
362
|
+
* personne ne lit celles des autres. Le stockage n'est pas touché : les valeurs héritées
|
|
363
|
+
* disparaissent d'elles-mêmes au premier basculement.
|
|
364
|
+
*/
|
|
365
|
+
function reactionsPubliques(brut) {
|
|
366
|
+
if (!brut || typeof brut !== "object" || Array.isArray(brut)) return brut;
|
|
367
|
+
const out = Object.create(null);
|
|
368
|
+
for (const [emoji, liste] of Object.entries(brut)) {
|
|
369
|
+
if (!Array.isArray(liste)) continue;
|
|
370
|
+
out[emoji] = liste.map((v, i) => (/^[0-9a-f]{16}$/.test(String(v)) ? String(v) : `ancien-${i}`));
|
|
371
|
+
}
|
|
372
|
+
return out;
|
|
373
|
+
}
|
|
374
|
+
|
|
253
375
|
function messagePublic(row) {
|
|
254
376
|
if (!row || typeof row !== "object") return null;
|
|
255
377
|
// Les clés viennent d'une liste blanche interne, mais l'objet est nu quand même : la règle se
|
|
256
378
|
// relit sans avoir à vérifier d'où vient chaque clé.
|
|
257
379
|
const out = Object.create(null);
|
|
258
380
|
for (const c of CHAMPS_PUBLICS) if (c in row) out[c] = row[c];
|
|
381
|
+
if ("reactions" in out) out.reactions = reactionsPubliques(out.reactions);
|
|
382
|
+
// Dérivé, jamais recopié : `author_hash` est lu pour ça et ne sort jamais tel quel.
|
|
383
|
+
if ("author_hash" in row) out.author_ref = refAuteur(row.author_hash);
|
|
259
384
|
return out;
|
|
260
385
|
}
|
|
261
386
|
|
|
@@ -266,6 +391,7 @@ function premierPublic(reponse) {
|
|
|
266
391
|
}
|
|
267
392
|
|
|
268
393
|
async function addMessage(slug, { name, email, avatar, isPresenter, isMember, body, replyTo, replyName, replyText, authorToken, attachment }) {
|
|
394
|
+
if (await estArchive(slug)) return REFUS_ARCHIVE;
|
|
269
395
|
const b = String(body || "").trim().slice(0, 2000);
|
|
270
396
|
// ⚠️ Pas de normalisation ici : `config.supabaseUrl` arrive SANS barre finale, c'est le
|
|
271
397
|
// contrat de l'adaptateur. En rajouter une deuxième couche, c'est deux endroits qui décident —
|
|
@@ -291,13 +417,24 @@ async function addMessage(slug, { name, email, avatar, isPresenter, isMember, bo
|
|
|
291
417
|
return { ok: true, message: premierPublic(cree) };
|
|
292
418
|
}
|
|
293
419
|
|
|
420
|
+
/**
|
|
421
|
+
* ⚠️ L'HISTORIQUE PASSE MAINTENANT PAR LA PROJECTION, et il ne le faisait pas. Il rendait les
|
|
422
|
+
* lignes TELLES QUELLES, en comptant sur la seule liste du `select` pour ne rien laisser filer.
|
|
423
|
+
* Ça tenait tant que la liste ne contenait que du public — puis la référence d'auteur a exigé de
|
|
424
|
+
* lire `author_hash`, et cette lecture serait ressortie intacte vers toute l'audience.
|
|
425
|
+
*
|
|
426
|
+
* Trouvé par l'essai écrit pour le correctif, avant le premier envoi : deux protections pour la
|
|
427
|
+
* même chose — un `select` étroit et une projection — dont une seule était appliquée ici. Une
|
|
428
|
+
* garde qui dépend de ce qu'on n'a pas demandé cède au premier champ qu'on demande.
|
|
429
|
+
*/
|
|
294
430
|
async function listMessages(slug) {
|
|
295
|
-
const rows = await PLAYER.db.request(`doc_presentation_messages?slug=eq.${enc(String(slug || ""))}&select=id,author_name,
|
|
296
|
-
return Array.isArray(rows) ? rows : [];
|
|
431
|
+
const rows = await PLAYER.db.request(`doc_presentation_messages?slug=eq.${enc(String(slug || ""))}&select=id,author_name,author_avatar,author_hash,is_presenter,is_member,body,attachment,reactions,reply_to,reply_name,reply_text,deleted,edited,created_at&order=created_at.asc&limit=300`);
|
|
432
|
+
return Array.isArray(rows) ? rows.map(messagePublic) : [];
|
|
297
433
|
}
|
|
298
434
|
|
|
299
435
|
// Éditer son message (jeton d'auteur requis). Ne touche pas aux messages supprimés.
|
|
300
436
|
async function editMessage(slug, msgId, authorToken, body) {
|
|
437
|
+
if (await estArchive(slug)) return REFUS_ARCHIVE;
|
|
301
438
|
const id = Math.trunc(+msgId); const b = String(body || "").trim().slice(0, 2000);
|
|
302
439
|
if (!id || !b || !authorToken) return { ok: false, status: 400 };
|
|
303
440
|
const rows = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&slug=eq.${enc(String(slug || ""))}&select=author_hash,deleted&limit=1`);
|
|
@@ -309,6 +446,7 @@ async function editMessage(slug, msgId, authorToken, body) {
|
|
|
309
446
|
|
|
310
447
|
// Supprimer (soft) : l'auteur (jeton) OU le présentateur (control_token de la présentation).
|
|
311
448
|
async function deleteMessage(slug, msgId, { authorToken, control }) {
|
|
449
|
+
if (await estArchive(slug)) return REFUS_ARCHIVE;
|
|
312
450
|
const id = Math.trunc(+msgId);
|
|
313
451
|
if (!id) return { ok: false, status: 400 };
|
|
314
452
|
const rows = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&slug=eq.${enc(String(slug || ""))}&select=author_hash&limit=1`);
|
|
@@ -323,6 +461,7 @@ async function deleteMessage(slug, msgId, { authorToken, control }) {
|
|
|
323
461
|
|
|
324
462
|
// Verrouiller / déverrouiller le chat (présentateur uniquement).
|
|
325
463
|
async function setChatLock(slug, control, locked) {
|
|
464
|
+
if (await estArchive(slug)) return REFUS_ARCHIVE;
|
|
326
465
|
const pres = await getPresentation(slug);
|
|
327
466
|
if (!pres) return { ok: false, status: 404 };
|
|
328
467
|
if (!tokenMatches(control, pres.control_hash)) return { ok: false, status: 403 };
|
|
@@ -332,6 +471,7 @@ async function setChatLock(slug, control, locked) {
|
|
|
332
471
|
|
|
333
472
|
// Réaction emoji (toggle) : le participant (identifié par email ou nom) ajoute/retire un emoji sur un message.
|
|
334
473
|
async function toggleReaction(slug, msgId, emoji, reactor) {
|
|
474
|
+
if (await estArchive(slug)) return REFUS_ARCHIVE;
|
|
335
475
|
const id = Math.trunc(+msgId); const e = String(emoji || "").slice(0, 8); const who = String(reactor || "").slice(0, 160).toLowerCase();
|
|
336
476
|
if (!id || !e || !who) return { ok: false, status: 400 };
|
|
337
477
|
// ⚠️ Une réaction est un EMOJI. Refuser tout ce qui ressemble à un identifiant ferme la porte à
|
|
@@ -372,6 +512,10 @@ async function recordAttendance(slug, { key, name, email, avatar, isMember, isPr
|
|
|
372
512
|
if (!slug || !key) return { ok: false, status: 400 };
|
|
373
513
|
const pres = await getPresentation(slug);
|
|
374
514
|
if (!pres) return { ok: false, status: 404 };
|
|
515
|
+
// ⚠️ La lecture est DÉJÀ faite ici : on s'en sert plutôt que d'en refaire une. Un battement de
|
|
516
|
+
// présence sur une session close n'a rien à mettre à jour — et il en arrive à chaque onglet
|
|
517
|
+
// resté ouvert, longtemps après la fin.
|
|
518
|
+
if (pres.active === false && pres.control_hash == null) return REFUS_ARCHIVE;
|
|
375
519
|
const page = Math.max(1, Math.trunc(Number(pres.current_page) || 1));
|
|
376
520
|
const now = Date.now();
|
|
377
521
|
const rows = await PLAYER.db.request(`doc_presentation_attendees?slug=eq.${enc(slug)}&attendee_key=eq.${enc(String(key))}&select=*&limit=1`);
|
|
@@ -459,10 +603,13 @@ async function switchPresentationDoc(slug, email, isAdmin, { fileUrl, fileName,
|
|
|
459
603
|
if (!row) return { ok: false, status: 404 };
|
|
460
604
|
if (estClose(row)) return { ok: false, status: 409, error: "ended" };
|
|
461
605
|
if (!isAdmin && (!row.owner_email || row.owner_email !== lc(email))) return { ok: false, status: 403 };
|
|
462
|
-
await
|
|
606
|
+
const ecrit = await ecrireSiEncoreVrai(`doc_presentations?slug=eq.${enc(slug)}&active=eq.true`, {
|
|
463
607
|
file_url: String(fileUrl), file_name: fileName || null, doc_title: docTitle || null, doc_id: docId ? String(docId) : null,
|
|
464
608
|
content: null, current_page: 1, active: true, last_seen: new Date().toISOString(), updated_at: new Date().toISOString(),
|
|
465
|
-
}
|
|
609
|
+
});
|
|
610
|
+
// Une présentation terminée entre le contrôle et l'écriture n'est pas rouverte par un
|
|
611
|
+
// changement de document : le refus arrive tard, mais il arrive.
|
|
612
|
+
if (!ecrit) return { ok: false, status: 409, error: "ended" };
|
|
466
613
|
return { ok: true };
|
|
467
614
|
}
|
|
468
615
|
|
|
@@ -517,7 +664,17 @@ async function setPresentationContent(slug, email, isAdmin, content, control) {
|
|
|
517
664
|
if (!pilote && !proprietaire) return { ok: false, status: 403 };
|
|
518
665
|
// Le pilote a déjà été filtré par la révocation du jeton ; c'est le propriétaire qu'on arrête ici.
|
|
519
666
|
if (!pilote && estClose(row)) return { ok: false, status: 409, error: "ended" };
|
|
520
|
-
|
|
667
|
+
// ⚠️ DEUX AUTORISATIONS, DONC DEUX CONDITIONS — et c'est la seule façon de ne pas retirer un
|
|
668
|
+
// droit en fermant une porte. Le PILOTE écrit tant que son jeton vaut : une carte affichée
|
|
669
|
+
// pendant que le portable dormait doit encore partir, comme une page. Le PROPRIÉTAIRE, lui,
|
|
670
|
+
// n'écrit que sur une présentation vivante : c'est déjà ce que le contrôle au-dessus exige.
|
|
671
|
+
const ecrit = await ecrireSiEncoreVrai(
|
|
672
|
+
pilote
|
|
673
|
+
? `doc_presentations?slug=eq.${enc(slug)}&control_hash=eq.${sha(control)}`
|
|
674
|
+
: `doc_presentations?slug=eq.${enc(slug)}&active=eq.true`,
|
|
675
|
+
{ content: sanitizeContent(content), active: true, last_seen: new Date().toISOString(), updated_at: new Date().toISOString() },
|
|
676
|
+
);
|
|
677
|
+
if (!ecrit) return { ok: false, status: 409, error: "ended" };
|
|
521
678
|
return { ok: true };
|
|
522
679
|
}
|
|
523
680
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
-- COMPTER SANS SE FAIRE DOUBLER.
|
|
2
|
+
--
|
|
3
|
+
-- Le compteur partagé faisait trois gestes : lire, calculer, écrire. Sous charge — c'est-à-dire
|
|
4
|
+
-- exactement quand une limite sert à quelque chose — plusieurs requêtes lisent le même compte et
|
|
5
|
+
-- écrivent chacune « ce compte + 1 ». Vingt requêtes simultanées sur une limite de dix passent
|
|
6
|
+
-- toutes. Une limite qui ne tient que lorsque personne ne pousse ne limite rien.
|
|
7
|
+
--
|
|
8
|
+
-- ⚠️ POURQUOI UNE FONCTION SQL ICI, alors que tout le reste du player tient en PostgREST simple.
|
|
9
|
+
-- L'incrément atomique n'est PAS exprimable en REST : `on conflict do update set count = count + 1`
|
|
10
|
+
-- a besoin de nommer la colonne des deux côtés, ce que le protocole ne permet pas. C'est la seule
|
|
11
|
+
-- opération du produit qui exige de dire quelque chose que REST ne sait pas dire — et la garde de
|
|
12
|
+
-- portabilité de la forge reste vraie, parce qu'un `rpc/` n'ajoute ni jointure ni arbre booléen.
|
|
13
|
+
--
|
|
14
|
+
-- ⚠️ LA FENÊTRE EST REMISE À NEUF, PAS PROLONGÉE. `expires_at` n'est réécrit que lorsque la
|
|
15
|
+
-- précédente est expirée : sans ça, chaque appel repousserait l'échéance et la fenêtre glisserait
|
|
16
|
+
-- indéfiniment — un visiteur régulier ne verrait jamais son compteur repartir de zéro.
|
|
17
|
+
--
|
|
18
|
+
-- Sans lui : le player laisse passer, et le dit une fois dans son journal en nommant ce fichier.
|
|
19
|
+
-- Les limites restent celles d'avant — comptées par instance et non atomiques — donc plusieurs
|
|
20
|
+
-- requêtes simultanées peuvent dépasser le plafond ensemble. Rien ne casse, rien ne se ferme : un
|
|
21
|
+
-- 429 raté coûte moins cher qu'une visionneuse morte, et une garde qui s'ouvre doit le dire.
|
|
22
|
+
--
|
|
23
|
+
-- Applicable pendant que la version précédente tourne : tant que le code ne l'appelle pas, elle
|
|
24
|
+
-- ne fait rien ; dès qu'il l'appelle, elle compte juste.
|
|
25
|
+
|
|
26
|
+
create or replace function public.player_rate_limit_bump(
|
|
27
|
+
p_key text,
|
|
28
|
+
p_max integer,
|
|
29
|
+
p_window_seconds integer
|
|
30
|
+
)
|
|
31
|
+
returns table (autorise boolean, compte integer)
|
|
32
|
+
language plpgsql
|
|
33
|
+
security definer
|
|
34
|
+
set search_path = public
|
|
35
|
+
as $$
|
|
36
|
+
declare
|
|
37
|
+
v_count integer;
|
|
38
|
+
begin
|
|
39
|
+
insert into public.player_rate_limits as l (key, count, expires_at)
|
|
40
|
+
values (p_key, 1, now() + make_interval(secs => p_window_seconds))
|
|
41
|
+
on conflict (key) do update
|
|
42
|
+
set count = case when l.expires_at <= now() then 1 else l.count + 1 end,
|
|
43
|
+
expires_at = case when l.expires_at <= now()
|
|
44
|
+
then now() + make_interval(secs => p_window_seconds)
|
|
45
|
+
else l.expires_at end
|
|
46
|
+
returning l.count into v_count;
|
|
47
|
+
|
|
48
|
+
-- Le refus est décidé ICI, sur la valeur qu'on vient d'écrire : le client n'a rien à recalculer,
|
|
49
|
+
-- donc rien à recalculer sur une valeur déjà périmée.
|
|
50
|
+
return query select (v_count <= p_max), v_count;
|
|
51
|
+
end;
|
|
52
|
+
$$;
|
|
53
|
+
|
|
54
|
+
-- Le player parle à la base avec la clé de service ; personne d'autre n'a à appeler ceci.
|
|
55
|
+
revoke all on function public.player_rate_limit_bump(text, integer, integer) from public, anon, authenticated;
|