discovery-media-player 0.1.21 → 0.1.23
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 +61 -0
- package/dist/bridge.d.ts +12 -3
- package/dist/bridge.js +42 -3
- package/package.json +1 -1
- package/server/browser.generated.js +1 -1
- package/server/handler.js +83 -13
- package/server/presentations.js +21 -1
- package/server/shares.js +38 -4
package/context/standalone.js
CHANGED
|
@@ -304,6 +304,65 @@ function createStandaloneContext(env = process.env) {
|
|
|
304
304
|
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
305
305
|
},
|
|
306
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Qui appelle ? **La socket, pas l'en-tête** — sauf si l'exploitant déclare son proxy.
|
|
309
|
+
*
|
|
310
|
+
* ⚠️ `X-Forwarded-For` est une affirmation du client. Toutes les limites de débit s'y
|
|
311
|
+
* appuyaient : un appelant direct — le cas du serveur autonome — en changeait à chaque
|
|
312
|
+
* requête et n'était jamais limité. La limite existait, elle ne limitait rien.
|
|
313
|
+
*
|
|
314
|
+
* `PLAYER_TRUSTED_PROXY_HOPS=1` dit « il y a UN proxy devant moi, sa dernière valeur ajoutée
|
|
315
|
+
* est l'IP réelle ». On lit alors depuis la FIN de la chaîne, pas depuis le début : le début
|
|
316
|
+
* est ce que le client a écrit, la fin est ce que les proxys ont constaté. C'est l'inverse
|
|
317
|
+
* de ce que faisait le code, et c'est l'erreur classique sur cet en-tête.
|
|
318
|
+
*
|
|
319
|
+
* Non configuré ⇒ l'en-tête est ignoré. Un hôte sans proxy est protégé sans rien faire.
|
|
320
|
+
*/
|
|
321
|
+
clientIp(req) {
|
|
322
|
+
const sauts = Math.max(0, Math.min(10, Number(env.PLAYER_TRUSTED_PROXY_HOPS || 0) || 0));
|
|
323
|
+
const socket = String((req && req.socket && req.socket.remoteAddress) || "");
|
|
324
|
+
if (!sauts) return socket;
|
|
325
|
+
const chaine = String((req && req.headers && req.headers["x-forwarded-for"]) || "")
|
|
326
|
+
.split(",").map((v) => v.trim()).filter(Boolean);
|
|
327
|
+
if (!chaine.length) return socket;
|
|
328
|
+
// Le proxy le plus proche a ajouté la dernière valeur : on remonte de `sauts` depuis la fin.
|
|
329
|
+
const i = chaine.length - sauts;
|
|
330
|
+
return chaine[i >= 0 ? i : 0] || socket;
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Vérifie un jeton de session INTERNE, émis par l'hôte pour son propre membre.
|
|
335
|
+
*
|
|
336
|
+
* ⚠️ POURQUOI DANS LE CORPS ET PAS EN EN-TÊTE. Le suivi de lecture part par `sendBeacon`,
|
|
337
|
+
* seul transport qui survive à la fermeture d'un onglet — et il ne sait pas porter
|
|
338
|
+
* d'en-tête. Exiger un JWT reviendrait à perdre la mesure au moment où elle compte le plus.
|
|
339
|
+
*
|
|
340
|
+
* Format : base64url(JSON) + "." + HMAC-SHA256 du même JSON, signé avec le secret que
|
|
341
|
+
* l'hôte détient déjà. Charge utile : { email, name, docId, exp }.
|
|
342
|
+
*
|
|
343
|
+
* ⚠️ Comparaison à temps constant, et `exp` obligatoire : un jeton sans expiration signé une
|
|
344
|
+
* fois vaudrait pour toujours, y compris après le départ du membre de l'entreprise.
|
|
345
|
+
*/
|
|
346
|
+
verifyInternalToken(jeton) {
|
|
347
|
+
const secret = String(env.PLAYER_HOST_FETCH_SECRET || "");
|
|
348
|
+
const brut = String(jeton || "");
|
|
349
|
+
if (!secret || !brut) return null;
|
|
350
|
+
const sep = brut.lastIndexOf(".");
|
|
351
|
+
if (sep <= 0) return null;
|
|
352
|
+
const corps = brut.slice(0, sep);
|
|
353
|
+
const signature = brut.slice(sep + 1);
|
|
354
|
+
try {
|
|
355
|
+
const attendue = crypto.createHmac("sha256", secret).update(corps).digest("base64url");
|
|
356
|
+
const a = Buffer.from(attendue, "utf8"); const b = Buffer.from(signature, "utf8");
|
|
357
|
+
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return null;
|
|
358
|
+
const d = JSON.parse(Buffer.from(corps, "base64url").toString("utf8"));
|
|
359
|
+
if (!d || typeof d !== "object") return null;
|
|
360
|
+
if (!Number.isFinite(d.exp) || d.exp * 1000 < Date.now()) return null;
|
|
361
|
+
if (!d.email || !d.docId) return null;
|
|
362
|
+
return { email: String(d.email), name: d.name ? String(d.name) : "", docId: String(d.docId) };
|
|
363
|
+
} catch { return null; }
|
|
364
|
+
},
|
|
365
|
+
|
|
307
366
|
// ⚠️ Le rôle vient d'`app_metadata`, JAMAIS d'`user_metadata` : ce dernier est modifiable par
|
|
308
367
|
// l'utilisateur lui-même. S'y fier revient à laisser chacun choisir ses droits.
|
|
309
368
|
roleOf: (user) => String(((user || {}).app_metadata || {}).role || "").trim().toLowerCase(),
|
|
@@ -408,6 +467,8 @@ function createStandaloneContext(env = process.env) {
|
|
|
408
467
|
// sont deux questions, et seule la seconde explique un refus.
|
|
409
468
|
hostShare: !!String(env.PLAYER_HOST_SHARE_SECRET || ""),
|
|
410
469
|
hostMail: !!String(env.PLAYER_HOST_MAIL_URL || "").trim(),
|
|
470
|
+
// Porte fermée : sans jeton de l'hôte, aucune session interne n'est écrite.
|
|
471
|
+
internalStrict: String(env.PLAYER_INTERNAL_STRICT || "") === "1",
|
|
411
472
|
},
|
|
412
473
|
};
|
|
413
474
|
}
|
package/dist/bridge.d.ts
CHANGED
|
@@ -41,8 +41,17 @@ export declare function parseHostMessage(data: unknown): HostMessage | null;
|
|
|
41
41
|
export declare function sendToHost(msg: PlayerMessage, target?: Window | null): void;
|
|
42
42
|
/** Côté HÔTE : envoie un message au player. */
|
|
43
43
|
export declare function sendToPlayer(frame: HTMLIFrameElement | null | undefined, msg: HostMessage): void;
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Côté HÔTE : écoute les messages du player. Renvoie la fonction de désabonnement.
|
|
46
|
+
*
|
|
47
|
+
* `expected` — l'iframe du player, ou sa fenêtre. **Recommandé** : sans lui, un autre cadre peut
|
|
48
|
+
* se faire passer pour le player.
|
|
49
|
+
*/
|
|
50
|
+
export declare function onPlayerMessage(cb: (msg: PlayerMessage) => void, win?: Window, expected?: HTMLIFrameElement | Window | null): () => void;
|
|
51
|
+
/**
|
|
52
|
+
* Côté PLAYER : écoute les messages de l'hôte. Renvoie la fonction de désabonnement.
|
|
53
|
+
*
|
|
54
|
+
* N'accepte que `window.parent` — le seul émetteur qui ait un sens dans un cadre.
|
|
55
|
+
*/
|
|
47
56
|
export declare function onHostMessage(cb: (msg: HostMessage) => void, win?: Window): () => void;
|
|
48
57
|
export {};
|
package/dist/bridge.js
CHANGED
|
@@ -88,12 +88,42 @@ export function sendToPlayer(frame, msg) {
|
|
|
88
88
|
}
|
|
89
89
|
catch { /* iframe déjà détruite */ }
|
|
90
90
|
}
|
|
91
|
-
/**
|
|
92
|
-
|
|
91
|
+
/**
|
|
92
|
+
* ⚠️ POURQUOI ON NE VÉRIFIE PAS L'ORIGINE, ET CE QU'ON VÉRIFIE À LA PLACE.
|
|
93
|
+
*
|
|
94
|
+
* Un contrôle d'origine est impossible ici : le player est encadré par des hôtes sur des domaines
|
|
95
|
+
* quelconques, déclarés à l'exploitation (`DOC_FRAME_ANCESTORS`), et il ne connaît pas celui de son
|
|
96
|
+
* hôte au moment où il écoute. C'est pour cette raison que le contrôle avait été écarté.
|
|
97
|
+
*
|
|
98
|
+
* Mais comparer la FENÊTRE SOURCE ne demande aucune origine. `e.source` est une référence : soit
|
|
99
|
+
* elle est celle qu'on attend, soit elle vient d'ailleurs. Sans ce test, n'importe quel onglet ou
|
|
100
|
+
* cadre détenant une référence à la fenêtre pouvait envoyer `close`, `share` ou `handover-done` —
|
|
101
|
+
* et la page les traitait comme venant de son hôte.
|
|
102
|
+
*
|
|
103
|
+
* Côté player, la fermeture est PAR DÉFAUT : le seul émetteur légitime est `window.parent`, aucun
|
|
104
|
+
* hôte n'a de code à changer. Côté hôte, le paramètre est optionnel — imposer une fermeture par
|
|
105
|
+
* défaut ferait taire les messages chez tous ceux qui ne l'auraient pas encore passé, et un
|
|
106
|
+
* message qui n'arrive plus est la pire façon d'annoncer un durcissement.
|
|
107
|
+
*/
|
|
108
|
+
function memeFenetre(attendue, source) {
|
|
109
|
+
if (!attendue)
|
|
110
|
+
return true; // rien d'attendu : on n'exclut rien
|
|
111
|
+
const w = attendue.contentWindow ?? attendue;
|
|
112
|
+
return source === w;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Côté HÔTE : écoute les messages du player. Renvoie la fonction de désabonnement.
|
|
116
|
+
*
|
|
117
|
+
* `expected` — l'iframe du player, ou sa fenêtre. **Recommandé** : sans lui, un autre cadre peut
|
|
118
|
+
* se faire passer pour le player.
|
|
119
|
+
*/
|
|
120
|
+
export function onPlayerMessage(cb, win, expected) {
|
|
93
121
|
const target = win || (typeof window !== "undefined" ? window : null);
|
|
94
122
|
if (!target)
|
|
95
123
|
return () => { };
|
|
96
124
|
const handler = (e) => {
|
|
125
|
+
if (!memeFenetre(expected, e.source))
|
|
126
|
+
return;
|
|
97
127
|
const msg = parsePlayerMessage(e.data);
|
|
98
128
|
if (msg)
|
|
99
129
|
cb(msg);
|
|
@@ -101,12 +131,21 @@ export function onPlayerMessage(cb, win) {
|
|
|
101
131
|
target.addEventListener("message", handler);
|
|
102
132
|
return () => target.removeEventListener("message", handler);
|
|
103
133
|
}
|
|
104
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* Côté PLAYER : écoute les messages de l'hôte. Renvoie la fonction de désabonnement.
|
|
136
|
+
*
|
|
137
|
+
* N'accepte que `window.parent` — le seul émetteur qui ait un sens dans un cadre.
|
|
138
|
+
*/
|
|
105
139
|
export function onHostMessage(cb, win) {
|
|
106
140
|
const target = win || (typeof window !== "undefined" ? window : null);
|
|
107
141
|
if (!target)
|
|
108
142
|
return () => { };
|
|
143
|
+
const parent = target.parent ?? null;
|
|
109
144
|
const handler = (e) => {
|
|
145
|
+
// ⚠️ `parent === target` quand la page n'est PAS encadrée : il n'y a alors pas d'hôte, donc
|
|
146
|
+
// rien à accepter — et surtout pas ses propres messages renvoyés par un script de la page.
|
|
147
|
+
if (!parent || parent === target || e.source !== parent)
|
|
148
|
+
return;
|
|
110
149
|
const msg = parseHostMessage(e.data);
|
|
111
150
|
if (msg)
|
|
112
151
|
cb(msg);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
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 U=Object.defineProperty;var ve=Object.getOwnPropertyDescriptor;var Me=Object.getOwnPropertyNames;var xe=Object.prototype.hasOwnProperty;var d=(e,n)=>{for(var t in n)U(e,t,{get:n[t],enumerable:!0})},we=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of Me(n))!xe.call(e,o)&&o!==t&&U(e,o,{get:()=>n[o],enumerable:!(r=ve(n,o))||r.enumerable});return e};var ke=e=>we(U({},\"__esModule\",{value:!0}),e);var pn={};d(pn,{bridge:()=>S,chat:()=>C,live:()=>I,presentation:()=>L,presentationState:()=>R,tracking:()=>A,viewer:()=>$});var S={};d(S,{onHostMessage:()=>Le,onPlayerMessage:()=>Ce,parseHostMessage:()=>de,parsePlayerMessage:()=>ce,sendToHost:()=>Ae,sendToPlayer:()=>Ie,toWire:()=>K});var V=\"3dd-doc-\",Te=/^[A-Za-z0-9_-]{1,64}$/,_e=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Pe=[\"embed-denied\"],Ee=[\"present-invite\",\"present-handover\",\"present-switch\"],Se=[\"handover-done\"];function K(e){let n={type:V+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 ue(e,n,t){if(!e||typeof e!=\"object\")return null;let r=e.type;if(typeof r!=\"string\"||!r.startsWith(V))return null;let o=r.slice(V.length);if(n.includes(o))return{type:o};if(Pe.includes(o)){let u=e.reason;return{type:o,reason:/^[a-z-]{1,40}$/.test(String(u||\"\"))?String(u):\"unknown\"}}if(!t.includes(o))return null;let i=e.slug;return typeof i!=\"string\"||!Te.test(i)?null:{type:o,slug:i}}function ce(e){return ue(e,_e,Ee)}function de(e){return ue(e,Se,[])}function Ae(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(K(e),\"*\")}catch(t){}}function Ie(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(K(n),\"*\")}catch(r){}}function Ce(e,n){let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=o=>{let i=ce(o.data);i&&e(i)};return t.addEventListener(\"message\",r),()=>t.removeEventListener(\"message\",r)}function Le(e,n){let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=o=>{let i=de(o.data);i&&e(i)};return t.addEventListener(\"message\",r),()=>t.removeEventListener(\"message\",r)}var A={};d(A,{createTracker:()=>Ne});var $e=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function Re(){return Math.random().toString(36).slice(2)+Date.now().toString(36)}function He(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 Ne(e={}){var ie,ae;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",i=(ie=e.idleMs)!=null?ie:6e4,u=(ae=e.sessionEveryMs)!=null?ae:12e3,y=e.slug||\"\",p=e.internal||null,H=e.sessionId||Re(),he=e.send||He(o,n),f={},v=0,N=0,M=0,ee=0,c=null,_=r(),x=!1,O=!1,P=[],ye=()=>!!y||!!p,ne=s=>{if(ye())try{he(s)}catch(a){}},te=()=>!!t&&t.visibilityState===\"visible\"&&(typeof t.hasFocus==\"function\"?t.hasFocus():!0)&&!x,re=s=>{let a=r()-s,m=Math.max(0,_-s)+i;return Math.max(0,Math.min(a,m))},W=()=>{c!=null&&v>0&&(f[v]=(f[v]||0)+re(c)/1e3),c=te()?r():null},D=()=>{W(),c=null},F=()=>{c==null&&te()&&(c=r())},z=()=>{let s=0;for(let a in f)s+=f[a];return c!=null&&(s+=re(c)/1e3),Math.round(s)},oe=()=>{let s={};for(let a in f)s[a]=Math.round(f[a]);return s},w=()=>{_=r(),x&&(x=!1,F())},se=(s,a)=>{p||ne({slug:y,event:s,page:a||N,maxPage:M,seconds:z(),sessionId:H})},E=()=>{W();let s={event:\"session\",sessionId:H,numPages:ee,maxPage:M,totalSeconds:z(),pagesTime:oe()};p?(s.internal=!0,s.docId=p.docId,s.email=p.email,s.name=p.name):s.slug=y,ne(s)},g=(s,a,m,le)=>{!s||typeof s.addEventListener!=\"function\"||(s.addEventListener(a,m,le),P.push(()=>{s.removeEventListener(a,m,le)}))};return{sessionId:H,totalSeconds:z,maxPage:()=>M,pageTimes:oe,start(){if(O)return;O=!0,se(\"open\",1),v=1,c=r(),_=r(),g(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(D(),E()):(w(),F())}),g(n,\"blur\",D),g(n,\"focus\",()=>{w(),F()});for(let m of $e)g(n,m,w,{passive:!0});g(e.scrollElement,\"scroll\",w,{passive:!0}),g(n,\"beforeunload\",E);let s=n.setInterval(()=>{!x&&r()-_>i&&(x=!0,D())},5e3),a=n.setInterval(E,u);P.push(()=>{n.clearInterval(s),n.clearInterval(a)})},setPage(s){!s||s===N||(W(),v=s,N=s,s>M&&(M=s,se(\"page\",s)))},setPageCount(s){ee=s||0},noteActivity:w,flush:E,stop(){for(;P.length;){let s=P.pop();try{s==null||s()}catch(a){}}O=!1}}}var I={};d(I,{STORAGE_KEYS:()=>k,attendeeKey:()=>Fe,authorToken:()=>ze,avatarHtml:()=>G,canModerate:()=>Z,escapeHtml:()=>l,flattenPresence:()=>Oe,formatMessageBody:()=>j,initials:()=>pe,isMentioned:()=>Ue,isMine:()=>b,reactorId:()=>B,shouldNotify:()=>Ve,unreadLabel:()=>Ke});var k={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\"};function l(e){return String(e==null?\"\":e).replace(/&/g,\"&\").replace(/</g,\"<\").replace(/>/g,\">\").replace(/\"/g,\""\")}function pe(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function G(e,n){return e?`<img src=\"${l(e)}\" alt=\"\">`:l(pe(n))}function j(e){let n=l(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 Oe(e){let n={},t=[];for(let r in e||{})for(let o of(e||{})[r]||[]){let i=String(o.uid||o.email||o.name||\"\").toLowerCase()||`_${r}_${t.length}`;n[i]?o.role===\"presenter\"&&(n[i]=o):(n[i]=o,t.push(i))}return t.map(r=>n[r])}function We(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function De(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function Fe(e,n,t=\"anon\",r=()=>Math.random().toString(36).slice(2,10)){if(e!=null&&e.email)return String(e.email).toLowerCase();try{if(!n)return`anon-${t}`;let o=n.getItem(k.attendeeKey);if(o)return o;let i=`anon-${r()}`;return n.setItem(k.attendeeKey,i),i}catch(o){return`anon-${t}`}}function ze(e,n=()=>Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)){let t=We(e,k.authorToken);if(t)return t;let r=n();return De(e,k.authorToken,r),r}function B(e){return String(e&&(e.email||e.name)||\"\").toLowerCase()}function b(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 Z(e){return!!(e&&e.role===\"presenter\")}function Ue(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}`)&&!b(e,n):!1}function Ve(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||b(n,t)?!1:o}function Ke(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}var C={};d(C,{messageClassName:()=>je,renderActions:()=>me,renderAttachment:()=>ge,renderMessage:()=>Ge,renderReactions:()=>fe});function fe(e,n){let t=e&&e.reactions||{},r=B(n),o=\"\";for(let i in t){let u=t[i]||[];if(!u.length)continue;let y=u.indexOf(r)>=0;o+=`<button class=\"re-chip${y?\" on\":\"\"}\" data-e=\"${l(i)}\">${l(i)} ${u.length}</button>`}return o}function ge(e){if(!e||!e.url)return\"\";let n=l(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=l(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 me(e,n){let t=b(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||Z(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function Ge(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>${l(e.reply_name||\"\")}</b> ${l(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${j(e.body)}${r}</div>`:\"\",i=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${G(e.author_avatar,e.author_name)}</span><span class=b><div class=who><b>${l(e.author_name||\"Invit\\xE9\")}</b>${i}</div>`+t+o+ge(e.attachment)+`<div class=cm-re>${fe(e,n.me)}</div></span><span class=cm-act>${me(e,n)}</span>`}function je(e,n,t){return\"cm\"+(b(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var L={};d(L,{DEFAULT_CENTER:()=>X,DEFAULT_ZOOM:()=>J,MAP_TYPES:()=>T,cycleMapType:()=>Be,initialMapContent:()=>Ye,mapTypeLabel:()=>Ze,sanitizeContent:()=>Q});var T=[\"roadmap\",\"satellite\",\"hybrid\"],X=[46.6,2.5],J=6;function h(e){let n=Number(e);return Number.isFinite(n)?n:null}function Y(e){if(!Array.isArray(e))return null;let n=h(e[0]),t=h(e[1]);return n!=null&&t!=null?[n,t]:null}var q=(e,n,t)=>Math.max(n,Math.min(t,e));function Q(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=Y(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:h(r.heading)||0,pitch:q(h(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:q(h(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:Y(n.center)||[...X],zoom:q(Math.trunc(h(n.zoom)||J),1,21),marker:Y(n.marker),mapType:T.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function Be(e){let n=T.indexOf(e);return T[(n+1)%T.length]}function Ze(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function Ye(){return{kind:\"map\",center:[...X],zoom:J,marker:null,mapType:null,label:null}}var $={};d($,{CURRENT_PAGE_MARGIN:()=>nn,DEFAULT_ASPECT:()=>Je,MAX_ZOOM:()=>Xe,MIN_PAGE_WIDTH:()=>Qe,MIN_ZOOM:()=>qe,PRERENDER_MARGIN:()=>en,arrowState:()=>on,averageColor:()=>ln,clampPage:()=>be,clampZoom:()=>tn,fitWidth:()=>an,isImageDocument:()=>un,progressPercent:()=>rn});var qe=.5,Xe=3,Je=1.35,Qe=280,en=\"500px 0px\",nn=\"-48% 0px -48% 0px\";function tn(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function be(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function rn(e,n){return n?Math.round(be(e,n)/n*100):0}function on(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var sn=28;function an(e){var u;let n=(u=e.zoom)!=null?u:1,t=Math.max(280,e.containerWidth||900)*n;if(!e.onePage)return t;let r=e.aspect||1.35,i=((e.containerHeight||600)-sn-(e.overlap||0)-(e.reserve||0))/r;return i>120?Math.min(t,i):t}function ln(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let i=0;i<e.length;i+=4)t+=e[i],r+=e[i+1],o+=e[i+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function un(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var R={};d(R,{presentationTransition:()=>cn,switchDocUrl:()=>dn});function cn(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=Q(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 dn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return ke(pn);})();", SOURCE_HASH: "51c05545eb241d37" };
|
|
7
|
+
module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var z=Object.defineProperty;var ve=Object.getOwnPropertyDescriptor;var Me=Object.getOwnPropertyNames;var xe=Object.prototype.hasOwnProperty;var p=(e,n)=>{for(var t in n)z(e,t,{get:n[t],enumerable:!0})},we=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of Me(n))!xe.call(e,o)&&o!==t&&z(e,o,{get:()=>n[o],enumerable:!(r=ve(n,o))||r.enumerable});return e};var ke=e=>we(z({},\"__esModule\",{value:!0}),e);var gn={};p(gn,{bridge:()=>I,chat:()=>C,live:()=>A,presentation:()=>L,presentationState:()=>$,tracking:()=>S,viewer:()=>R});var I={};p(I,{onHostMessage:()=>Re,onPlayerMessage:()=>Le,parseHostMessage:()=>de,parsePlayerMessage:()=>ce,sendToHost:()=>Se,sendToPlayer:()=>Ae,toWire:()=>j});var V=\"3dd-doc-\",Te=/^[A-Za-z0-9_-]{1,64}$/,_e=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Ee=[\"embed-denied\"],Pe=[\"present-invite\",\"present-handover\",\"present-switch\"],Ie=[\"handover-done\"];function j(e){let n={type:V+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 ue(e,n,t){if(!e||typeof e!=\"object\")return null;let r=e.type;if(typeof r!=\"string\"||!r.startsWith(V))return null;let o=r.slice(V.length);if(n.includes(o))return{type:o};if(Ee.includes(o)){let a=e.reason;return{type:o,reason:/^[a-z-]{1,40}$/.test(String(a||\"\"))?String(a):\"unknown\"}}if(!t.includes(o))return null;let i=e.slug;return typeof i!=\"string\"||!Te.test(i)?null:{type:o,slug:i}}function ce(e){return ue(e,_e,Pe)}function de(e){return ue(e,Ie,[])}function Se(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(j(e),\"*\")}catch(t){}}function Ae(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(j(n),\"*\")}catch(r){}}function Ce(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function Le(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=i=>{if(!Ce(t,i.source))return;let a=ce(i.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function Re(e,n){var i;let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=(i=t.parent)!=null?i:null,o=a=>{if(!r||r===t||a.source!==r)return;let d=de(a.data);d&&e(d)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var S={};p(S,{createTracker:()=>De});var $e=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function He(){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 We(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 De(e={}){var ie,ae;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",i=(ie=e.idleMs)!=null?ie:6e4,a=(ae=e.sessionEveryMs)!=null?ae:12e3,d=e.slug||\"\",f=e.internal||null,H=e.sessionId||He(),ye=e.send||We(o,n),g={},v=0,W=0,M=0,ee=0,c=null,_=r(),x=!1,D=!1,E=[],he=()=>!!d||!!f,ne=s=>{if(he())try{ye(s)}catch(l){}},te=()=>!!t&&t.visibilityState===\"visible\"&&(typeof t.hasFocus==\"function\"?t.hasFocus():!0)&&!x,re=s=>{let l=r()-s,b=Math.max(0,_-s)+i;return Math.max(0,Math.min(l,b))},N=()=>{c!=null&&v>0&&(g[v]=(g[v]||0)+re(c)/1e3),c=te()?r():null},O=()=>{N(),c=null},U=()=>{c==null&&te()&&(c=r())},F=()=>{let s=0;for(let l in g)s+=g[l];return c!=null&&(s+=re(c)/1e3),Math.round(s)},oe=()=>{let s={};for(let l in g)s[l]=Math.round(g[l]);return s},w=()=>{_=r(),x&&(x=!1,U())},se=(s,l)=>{f||ne({slug:d,event:s,page:l||W,maxPage:M,seconds:F(),sessionId:H})},P=()=>{N();let s={event:\"session\",sessionId:H,numPages:ee,maxPage:M,totalSeconds:F(),pagesTime:oe()};f?(s.internal=!0,s.docId=f.docId,s.email=f.email,s.name=f.name):s.slug=d,ne(s)},m=(s,l,b,le)=>{!s||typeof s.addEventListener!=\"function\"||(s.addEventListener(l,b,le),E.push(()=>{s.removeEventListener(l,b,le)}))};return{sessionId:H,totalSeconds:F,maxPage:()=>M,pageTimes:oe,start(){if(D)return;D=!0,se(\"open\",1),v=1,c=r(),_=r(),m(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(O(),P()):(w(),U())}),m(n,\"blur\",O),m(n,\"focus\",()=>{w(),U()});for(let b of $e)m(n,b,w,{passive:!0});m(e.scrollElement,\"scroll\",w,{passive:!0}),m(n,\"beforeunload\",P);let s=n.setInterval(()=>{!x&&r()-_>i&&(x=!0,O())},5e3),l=n.setInterval(P,a);E.push(()=>{n.clearInterval(s),n.clearInterval(l)})},setPage(s){!s||s===W||(N(),v=s,W=s,s>M&&(M=s,se(\"page\",s)))},setPageCount(s){ee=s||0},noteActivity:w,flush:P,stop(){for(;E.length;){let s=E.pop();try{s==null||s()}catch(l){}}D=!1}}}var A={};p(A,{STORAGE_KEYS:()=>k,attendeeKey:()=>Fe,authorToken:()=>Ve,avatarHtml:()=>K,canModerate:()=>Z,escapeHtml:()=>u,flattenPresence:()=>Ne,formatMessageBody:()=>G,initials:()=>pe,isMentioned:()=>je,isMine:()=>y,reactorId:()=>B,shouldNotify:()=>Ke,unreadLabel:()=>Ge});var k={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\"};function u(e){return String(e==null?\"\":e).replace(/&/g,\"&\").replace(/</g,\"<\").replace(/>/g,\">\").replace(/\"/g,\""\")}function pe(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function K(e,n){return e?`<img src=\"${u(e)}\" alt=\"\">`:u(pe(n))}function G(e){let n=u(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 Ne(e){let n={},t=[];for(let r in e||{})for(let o of(e||{})[r]||[]){let i=String(o.uid||o.email||o.name||\"\").toLowerCase()||`_${r}_${t.length}`;n[i]?o.role===\"presenter\"&&(n[i]=o):(n[i]=o,t.push(i))}return t.map(r=>n[r])}function Oe(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function Ue(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function Fe(e,n,t=\"anon\",r=()=>Math.random().toString(36).slice(2,10)){if(e!=null&&e.email)return String(e.email).toLowerCase();try{if(!n)return`anon-${t}`;let o=n.getItem(k.attendeeKey);if(o)return o;let i=`anon-${r()}`;return n.setItem(k.attendeeKey,i),i}catch(o){return`anon-${t}`}}function ze(){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 Ve(e,n=ze){let t=Oe(e,k.authorToken);if(t)return t;let r=n();return Ue(e,k.authorToken,r),r}function B(e){return String(e&&(e.email||e.name)||\"\").toLowerCase()}function y(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 Z(e){return!!(e&&e.role===\"presenter\")}function je(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}`)&&!y(e,n):!1}function Ke(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||y(n,t)?!1:o}function Ge(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}var C={};p(C,{messageClassName:()=>Ze,renderActions:()=>me,renderAttachment:()=>ge,renderMessage:()=>Be,renderReactions:()=>fe});function fe(e,n){let t=e&&e.reactions||{},r=B(n),o=\"\";for(let i in t){let a=t[i]||[];if(!a.length)continue;let d=a.indexOf(r)>=0;o+=`<button class=\"re-chip${d?\" on\":\"\"}\" data-e=\"${u(i)}\">${u(i)} ${a.length}</button>`}return o}function ge(e){if(!e||!e.url)return\"\";let n=u(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=u(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 me(e,n){let t=y(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||Z(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function Be(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>${u(e.reply_name||\"\")}</b> ${u(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${G(e.body)}${r}</div>`:\"\",i=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${K(e.author_avatar,e.author_name)}</span><span class=b><div class=who><b>${u(e.author_name||\"Invit\\xE9\")}</b>${i}</div>`+t+o+ge(e.attachment)+`<div class=cm-re>${fe(e,n.me)}</div></span><span class=cm-act>${me(e,n)}</span>`}function Ze(e,n,t){return\"cm\"+(y(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var L={};p(L,{DEFAULT_CENTER:()=>X,DEFAULT_ZOOM:()=>J,MAP_TYPES:()=>T,cycleMapType:()=>Ye,initialMapContent:()=>Xe,mapTypeLabel:()=>qe,sanitizeContent:()=>Q});var T=[\"roadmap\",\"satellite\",\"hybrid\"],X=[46.6,2.5],J=6;function h(e){let n=Number(e);return Number.isFinite(n)?n:null}function Y(e){if(!Array.isArray(e))return null;let n=h(e[0]),t=h(e[1]);return n!=null&&t!=null?[n,t]:null}var q=(e,n,t)=>Math.max(n,Math.min(t,e));function Q(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=Y(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:h(r.heading)||0,pitch:q(h(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:q(h(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:Y(n.center)||[...X],zoom:q(Math.trunc(h(n.zoom)||J),1,21),marker:Y(n.marker),mapType:T.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function Ye(e){let n=T.indexOf(e);return T[(n+1)%T.length]}function qe(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function Xe(){return{kind:\"map\",center:[...X],zoom:J,marker:null,mapType:null,label:null}}var R={};p(R,{CURRENT_PAGE_MARGIN:()=>rn,DEFAULT_ASPECT:()=>en,MAX_ZOOM:()=>Qe,MIN_PAGE_WIDTH:()=>nn,MIN_ZOOM:()=>Je,PRERENDER_MARGIN:()=>tn,arrowState:()=>an,averageColor:()=>cn,clampPage:()=>be,clampZoom:()=>on,fitWidth:()=>un,isImageDocument:()=>dn,progressPercent:()=>sn});var Je=.5,Qe=3,en=1.35,nn=280,tn=\"500px 0px\",rn=\"-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 be(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function sn(e,n){return n?Math.round(be(e,n)/n*100):0}function an(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var ln=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,i=((e.containerHeight||600)-ln-(e.overlap||0)-(e.reserve||0))/r;return i>120?Math.min(t,i):t}function cn(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let i=0;i<e.length;i+=4)t+=e[i],r+=e[i+1],o+=e[i+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function dn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var $={};p($,{presentationTransition:()=>pn,switchDocUrl:()=>fn});function pn(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=Q(e.content);if(t)return[{kind:\"show-map\",content:t}];let r=[{kind:\"leave-map\"}];return e.file_url&&e.file_url!==n.docUrl?(r.push({kind:\"switch-doc\",url:e.file_url,name:e.file_name||void 0,title:e.doc_title||void 0,updatedAt:e.updated_at||void 0}),r):(e.current_page&&r.push({kind:\"show-page\",page:e.current_page}),r)}function fn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return ke(gn);})();", SOURCE_HASH: "951763e203aba2b4" };
|
package/server/handler.js
CHANGED
|
@@ -1182,6 +1182,31 @@ function botMarkup(share, pitch) {
|
|
|
1182
1182
|
// formats du README ne connaît que le PDF et les images bitmap).
|
|
1183
1183
|
const TYPES_EXECUTABLES = /^(image\/svg|text\/html|application\/xhtml|application\/xml|text\/xml)/i;
|
|
1184
1184
|
|
|
1185
|
+
// ⚠️ `X-Forwarded-For` EST UN EN-TÊTE, DONC UNE AFFIRMATION DU CLIENT.
|
|
1186
|
+
//
|
|
1187
|
+
// Onze endroits en prenaient la première valeur pour identifier l'appelant, et toutes les limites
|
|
1188
|
+
// de débit s'appuyaient dessus. Un client qui atteint directement le serveur — c'est le cas du
|
|
1189
|
+
// serveur autonome, et de toute instance dont le proxy ne réécrit pas cet en-tête — pouvait donc
|
|
1190
|
+
// en changer à chaque requête et n'être jamais limité. La limite existait, elle ne limitait rien.
|
|
1191
|
+
//
|
|
1192
|
+
// Le caractère « par processus » des compteurs était documenté ; la confiance dans un en-tête non
|
|
1193
|
+
// authentifié ne l'était pas. C'est la différence entre une limite approximative et une limite
|
|
1194
|
+
// décorative.
|
|
1195
|
+
//
|
|
1196
|
+
// La décision revient à l'hôte, parce que lui seul sait s'il y a un proxy devant lui :
|
|
1197
|
+
// `identity.clientIp(req)` s'il la fournit, sinon l'adresse de la socket — jamais l'en-tête. Un
|
|
1198
|
+
// hôte derrière un proxy déclare sa politique dans son câblage ; un hôte sans proxy n'a rien à
|
|
1199
|
+
// faire, et il est protégé par défaut.
|
|
1200
|
+
function adresseAppelant(req) {
|
|
1201
|
+
try {
|
|
1202
|
+
if (PLAYER.identity && typeof PLAYER.identity.clientIp === "function") {
|
|
1203
|
+
const v = PLAYER.identity.clientIp(req);
|
|
1204
|
+
if (v) return String(v).slice(0, 60);
|
|
1205
|
+
}
|
|
1206
|
+
} catch { /* une IP indisponible ne doit pas empêcher de lire un document */ }
|
|
1207
|
+
return String((req.socket && req.socket.remoteAddress) || "").slice(0, 60);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1185
1210
|
async function relayerFichier(res, r, disposition) {
|
|
1186
1211
|
if (!r) { res.statusCode = 404; res.end("Fichier indisponible"); return; }
|
|
1187
1212
|
if (!r.ok && r.status !== 206) { res.statusCode = 502; res.end("Fichier indisponible"); return; }
|
|
@@ -2337,7 +2362,7 @@ async function handler(req, res) {
|
|
|
2337
2362
|
const jv = (status, obj, cookie) => { res.statusCode = status; res.setHeader("Content-Type", "application/json"); if (cookie) res.setHeader("Set-Cookie", cookie); res.end(JSON.stringify(obj)); };
|
|
2338
2363
|
const V = PLAYER.plugins.visitors;
|
|
2339
2364
|
if (!V) return jv(404, { ok: false, error: "disabled" });
|
|
2340
|
-
const ip =
|
|
2365
|
+
const ip = adresseAppelant(req) || "ip";
|
|
2341
2366
|
if (body.action === "visitor-request") {
|
|
2342
2367
|
if (!(await PLAYER.limits.allow(`vcode:${ip}`, 20, 3600))) return jv(429, { ok: false, error: "rate" });
|
|
2343
2368
|
const sh = await getShareBySlug(String(body.slug || ""));
|
|
@@ -2365,7 +2390,7 @@ async function handler(req, res) {
|
|
|
2365
2390
|
try {
|
|
2366
2391
|
if (body.action === "present-start") {
|
|
2367
2392
|
if (!isAllowedStorageUrl(String(body.fileUrl || ""))) return jp(400, { ok: false, error: "url" });
|
|
2368
|
-
const ip =
|
|
2393
|
+
const ip = adresseAppelant(req) || "anon";
|
|
2369
2394
|
const allowed = await PLAYER.limits.allow(`pstart:${ip}`, 60, 3600);
|
|
2370
2395
|
if (!allowed) return jp(429, { ok: false, error: "rate" });
|
|
2371
2396
|
// On rattache la présentation au membre (JWT) → reprise / liste / transfert. Best-effort : sans
|
|
@@ -2398,7 +2423,7 @@ async function handler(req, res) {
|
|
|
2398
2423
|
if (!share || !share.bot_enabled) return jp(404, { ok: false, error: "bot" });
|
|
2399
2424
|
const text = String(body.text || "").replace(/\s+/g, " ").trim().slice(0, 700);
|
|
2400
2425
|
if (!text) return jp(400, { ok: false, error: "empty" });
|
|
2401
|
-
const ip =
|
|
2426
|
+
const ip = adresseAppelant(req) || "anon";
|
|
2402
2427
|
if (!(await PLAYER.limits.allow(`doctts:${ip}`, 400, 3600))) return jp(429, { ok: false, error: "rate" });
|
|
2403
2428
|
const defaultVoiceId = process.env.ELEVENLABS_VOICE_ID || "21m00Tcm4TlvDq8ikWAM";
|
|
2404
2429
|
let voiceId = defaultVoiceId;
|
|
@@ -2471,7 +2496,7 @@ async function handler(req, res) {
|
|
|
2471
2496
|
const jp = (status, obj) => { res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(obj)); };
|
|
2472
2497
|
if (!docbot) return jp(404, { ok: false, error: "disabled" });
|
|
2473
2498
|
try {
|
|
2474
|
-
const ip =
|
|
2499
|
+
const ip = adresseAppelant(req) || "anon";
|
|
2475
2500
|
const allowed = await PLAYER.limits.allow(`docbot:${ip}`, 120, 3600);
|
|
2476
2501
|
if (!allowed) return jp(429, { ok: false, error: "rate" });
|
|
2477
2502
|
const share = await getShareBySlug(String(body.slug || ""));
|
|
@@ -2520,7 +2545,7 @@ async function handler(req, res) {
|
|
|
2520
2545
|
if (body.action === "present-attend") {
|
|
2521
2546
|
const jp = (status, obj) => { res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(obj)); };
|
|
2522
2547
|
try {
|
|
2523
|
-
const ip =
|
|
2548
|
+
const ip = adresseAppelant(req) || "anon";
|
|
2524
2549
|
const allowed = await PLAYER.limits.allow(`patt:${ip}`, 1000, 3600);
|
|
2525
2550
|
if (!allowed) return jp(429, { ok: false, error: "rate" });
|
|
2526
2551
|
const r = await recordAttendance(String(body.slug || ""), { key: body.key, name: body.name, email: body.email, avatar: body.avatar, isMember: !!body.isMember, isPresenter: !!body.isPresenter });
|
|
@@ -2556,7 +2581,7 @@ async function handler(req, res) {
|
|
|
2556
2581
|
try {
|
|
2557
2582
|
const pres = await getPresentation(String(body.slug || ""));
|
|
2558
2583
|
if (!pres) return jp(404, { ok: false });
|
|
2559
|
-
const ip =
|
|
2584
|
+
const ip = adresseAppelant(req) || "anon";
|
|
2560
2585
|
const allowed = await PLAYER.limits.allow(`pchat:${ip}`, 60, 3600);
|
|
2561
2586
|
if (!allowed) return jp(429, { ok: false, error: "rate" });
|
|
2562
2587
|
// Le badge « présentateur » n'est accordé QUE si le control_token est valide (sinon n'importe quel
|
|
@@ -2573,7 +2598,7 @@ async function handler(req, res) {
|
|
|
2573
2598
|
try {
|
|
2574
2599
|
const pres = await getPresentation(String(body.slug || ""));
|
|
2575
2600
|
if (!pres) return jp(404, { ok: false });
|
|
2576
|
-
const ip =
|
|
2601
|
+
const ip = adresseAppelant(req) || "anon";
|
|
2577
2602
|
const allowed = await PLAYER.limits.allow(`pup:${ip}`, 30, 3600);
|
|
2578
2603
|
if (!allowed) return jp(429, { ok: false, error: "rate" });
|
|
2579
2604
|
const r = await createUploadUrl(String(body.slug || ""), body.name, body.type);
|
|
@@ -2595,7 +2620,7 @@ async function handler(req, res) {
|
|
|
2595
2620
|
if (body.action === "present-react") {
|
|
2596
2621
|
const jp = (status, obj) => { res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(obj)); };
|
|
2597
2622
|
try {
|
|
2598
|
-
const ip =
|
|
2623
|
+
const ip = adresseAppelant(req) || "anon";
|
|
2599
2624
|
const allowed = await PLAYER.limits.allow(`preact:${ip}`, 200, 3600);
|
|
2600
2625
|
if (!allowed) return jp(429, { ok: false, error: "rate" });
|
|
2601
2626
|
const r = await toggleReaction(String(body.slug || ""), body.msgId, body.emoji, body.reactor);
|
|
@@ -2645,7 +2670,7 @@ async function handler(req, res) {
|
|
|
2645
2670
|
const docId = String(body.docId || "").trim();
|
|
2646
2671
|
if (!docId || !body.fileUrl) return jd(400, { ok: false, error: "docId/fileUrl requis" });
|
|
2647
2672
|
|
|
2648
|
-
const ipH =
|
|
2673
|
+
const ipH = adresseAppelant(req) || "hote";
|
|
2649
2674
|
if (!(await PLAYER.limits.allow(`hshare:${ipH}`, 120, 3600))) return jd(429, { ok: false, error: "rate" });
|
|
2650
2675
|
|
|
2651
2676
|
// ⚠️ La clé d'idempotence n'a PAS demandé de colonne : « le lien de l'hôte pour ce
|
|
@@ -2731,7 +2756,7 @@ async function handler(req, res) {
|
|
|
2731
2756
|
const mail = String(body.email || "").trim().toLowerCase();
|
|
2732
2757
|
const j = (status, obj) => { res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(obj)); };
|
|
2733
2758
|
if (!/.+@.+\..+/.test(mail)) return j(400, { ok: false, error: "email" });
|
|
2734
|
-
const ip =
|
|
2759
|
+
const ip = adresseAppelant(req) || "anon";
|
|
2735
2760
|
const allowed = await PLAYER.limits.allow(`reshare:${ip}`, 8, 3600);
|
|
2736
2761
|
if (!allowed) return j(429, { ok: false, error: "rate", message: "Trop de partages, réessayez plus tard." });
|
|
2737
2762
|
let out = null;
|
|
@@ -2790,10 +2815,55 @@ async function handler(req, res) {
|
|
|
2790
2815
|
return j(200, { ok: true, slug: out.slug, sent, ...(refusEnvoi ? { sendRefused: refusEnvoi } : {}) });
|
|
2791
2816
|
}
|
|
2792
2817
|
const ua0 = req.headers["user-agent"];
|
|
2793
|
-
const ip0 =
|
|
2794
|
-
//
|
|
2818
|
+
const ip0 = adresseAppelant(req);
|
|
2819
|
+
// ⚠️ CONSULTATION INTERNE : L'IDENTITÉ EST AFFIRMÉE PAR LE NAVIGATEUR, PAS PROUVÉE.
|
|
2820
|
+
//
|
|
2821
|
+
// La population interne est celle que le produit promet de ne jamais mélanger aux prospects.
|
|
2822
|
+
// Or cette route acceptait n'importe quel e-mail, n'importe quel document, sans jeton ni
|
|
2823
|
+
// limite : on pouvait fabriquer « tel collègue a lu ce document trois heures ».
|
|
2824
|
+
//
|
|
2825
|
+
// Le suivi part par `sendBeacon`, qui ne sait pas porter d'en-tête — exiger un JWT casserait
|
|
2826
|
+
// le seul transport qui survive à la fermeture d'un onglet, c'est-à-dire le moment où la
|
|
2827
|
+
// mesure compte le plus. La preuve doit donc voyager dans le CORPS, et venir de l'hôte :
|
|
2828
|
+
// lui seul sait qui est son membre.
|
|
2829
|
+
//
|
|
2830
|
+
// `PLAYER_INTERNAL_STRICT` laisse chaque hôte fermer la porte à son rythme :
|
|
2831
|
+
// • absent → on accepte, borné et limité, et on le SIGNALE une fois de temps en temps ;
|
|
2832
|
+
// • posé → sans jeton valide, rien n'est écrit.
|
|
2833
|
+
// Une fermeture par défaut casserait les instances en service, dont la nôtre. Une porte
|
|
2834
|
+
// qu'on laisse ouverte sans le dire est un défaut ; une porte qu'on laisse ouverte en le
|
|
2835
|
+
// disant, avec le verrou fourni, est une transition.
|
|
2795
2836
|
if (body.internal && body.event === "session") {
|
|
2796
|
-
|
|
2837
|
+
const ipInt = ip0 || "anon";
|
|
2838
|
+
if (!(await PLAYER.limits.allow(`intsess:${ipInt}`, 120, 3600))) {
|
|
2839
|
+
res.statusCode = 429; res.setHeader("Content-Type", "application/json"); res.end('{"ok":false,"error":"rate"}');
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
const jeton = typeof PLAYER.identity.verifyInternalToken === "function"
|
|
2843
|
+
? PLAYER.identity.verifyInternalToken(String(body.it || ""))
|
|
2844
|
+
: null;
|
|
2845
|
+
const strict = !!(PLAYER.config && PLAYER.config.internalStrict);
|
|
2846
|
+
if (strict && !jeton) {
|
|
2847
|
+
res.statusCode = 403; res.setHeader("Content-Type", "application/json"); res.end('{"ok":false,"error":"internal-token"}');
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
if (!strict && !jeton) {
|
|
2851
|
+
// Une fois par heure et par instance : assez pour être vu dans les journaux, pas assez
|
|
2852
|
+
// pour les noyer — un avertissement répété à chaque battement ne se lit plus.
|
|
2853
|
+
if (await PLAYER.limits.allow("intsess:avert", 1, 3600)) {
|
|
2854
|
+
try { PLAYER.errors.capture(new Error("session interne écrite sans jeton : l'identité vient du navigateur. Poser PLAYER_INTERNAL_STRICT=1 une fois l'hôte à jour"), { route: "internal-session" }); } catch { /* ignore */ }
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
// Le jeton fait foi quand il est là : c'est l'hôte qui se porte garant, pas l'appelant.
|
|
2858
|
+
try {
|
|
2859
|
+
await upsertInternalSession({
|
|
2860
|
+
sessionId: body.sessionId,
|
|
2861
|
+
docId: jeton ? jeton.docId : body.docId,
|
|
2862
|
+
userEmail: jeton ? jeton.email : body.email,
|
|
2863
|
+
userName: jeton ? (jeton.name || body.name) : body.name,
|
|
2864
|
+
numPages: body.numPages, maxPage: body.maxPage, totalSeconds: body.totalSeconds, pagesTime: body.pagesTime,
|
|
2865
|
+
}, { ip: ip0, ua: ua0 });
|
|
2866
|
+
} catch { /* best-effort */ }
|
|
2797
2867
|
res.statusCode = 200; res.setHeader("Content-Type", "application/json"); res.end('{"ok":true}');
|
|
2798
2868
|
return;
|
|
2799
2869
|
}
|
package/server/presentations.js
CHANGED
|
@@ -251,8 +251,28 @@ async function setChatLock(slug, control, locked) {
|
|
|
251
251
|
async function toggleReaction(slug, msgId, emoji, reactor) {
|
|
252
252
|
const id = Math.trunc(+msgId); const e = String(emoji || "").slice(0, 8); const who = String(reactor || "").slice(0, 160).toLowerCase();
|
|
253
253
|
if (!id || !e || !who) return { ok: false, status: 400 };
|
|
254
|
+
// ⚠️ Une réaction est un EMOJI. Refuser tout ce qui ressemble à un identifiant ferme la porte à
|
|
255
|
+
// la source, en plus de l'objet sans prototype : `toString` stocké resterait affiché aux
|
|
256
|
+
// participants comme une réaction, ce qui est absurde même sans être dangereux. Deux barrières,
|
|
257
|
+
// parce que la seconde protège le jour où quelqu'un lève le plafond de longueur.
|
|
258
|
+
if (/[A-Za-z_$]/.test(e)) return { ok: false, status: 400 };
|
|
254
259
|
const rows = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&slug=eq.${enc(String(slug || ""))}&select=reactions&limit=1`);
|
|
255
|
-
|
|
260
|
+
// ⚠️ LE NOM DE PROPRIÉTÉ ÉCRIT ICI VIENT DU CLIENT — et la garde posée en 0.1.2 ne couvrait que
|
|
261
|
+
// les LECTURES. `Object.hasOwn` empêchait de LIRE `constructor` ; rien n'empêchait de l'ÉCRIRE.
|
|
262
|
+
//
|
|
263
|
+
// Le plafond de 8 caractères bloquait le pire par accident : `__proto__` (9) et `constructor`
|
|
264
|
+
// (11) sont tronqués en clés inoffensives. Mais `toString` (8) et `valueOf` (7) passaient, et
|
|
265
|
+
// devenaient des propriétés PROPRES de l'objet stocké — masquant celles du prototype pour tout
|
|
266
|
+
// consommateur, y compris le navigateur qui itère cet objet pour dessiner les réactions.
|
|
267
|
+
//
|
|
268
|
+
// ⚠️ Et cette protection accidentelle est fragile : les emojis composés (famille, drapeaux
|
|
269
|
+
// régionaux, séquences ZWJ) dépassent 8 caractères. Le jour où quelqu'un lèvera le plafond pour
|
|
270
|
+
// les accepter — un changement d'apparence anodin — `__proto__` et `constructor` passeront avec.
|
|
271
|
+
//
|
|
272
|
+
// Un objet SANS prototype retire la classe entière : il n'y a plus rien à masquer ni à
|
|
273
|
+
// atteindre, quelle que soit la clé et quel que soit le plafond.
|
|
274
|
+
const brut = (Array.isArray(rows) && rows[0] && rows[0].reactions && typeof rows[0].reactions === "object" && !Array.isArray(rows[0].reactions)) ? rows[0].reactions : {};
|
|
275
|
+
const cur = Object.assign(Object.create(null), brut);
|
|
256
276
|
const arr = Array.isArray(cur[e]) ? cur[e] : [];
|
|
257
277
|
const i = arr.indexOf(who);
|
|
258
278
|
if (i >= 0) arr.splice(i, 1); else arr.push(who);
|
package/server/shares.js
CHANGED
|
@@ -202,7 +202,12 @@ async function overview() {
|
|
|
202
202
|
|
|
203
203
|
// Parse minimal d'un User-Agent → appareil / OS / navigateur (sans dépendance).
|
|
204
204
|
function parseUa(ua) {
|
|
205
|
-
|
|
205
|
+
// ⚠️ Bornée AVANT analyse. L'analyse statique signale `Android.*Mobile` comme pouvant revenir en
|
|
206
|
+
// arrière ; mesuré avant de corriger, V8 traite ce motif en temps linéaire même sur 200 000
|
|
207
|
+
// caractères — ce n'était donc pas une lenteur réelle. On borne quand même : la colonne stockée
|
|
208
|
+
// était déjà tronquée à 300, seule l'analyse recevait la chaîne entière. Faire entrer une
|
|
209
|
+
// longueur non bornée dans une expression régulière est une habitude qui finit par coûter.
|
|
210
|
+
const s = String(ua || "").slice(0, 300);
|
|
206
211
|
let device = /Mobile|iPhone|Android.*Mobile/.test(s) ? "Mobile" : /iPad|Tablet/.test(s) ? "Tablette" : "Ordinateur";
|
|
207
212
|
let os = /Windows/.test(s) ? "Windows" : /iPhone|iPad|iOS/.test(s) ? "iOS" : /Mac OS X|Macintosh/.test(s) ? "macOS" : /Android/.test(s) ? "Android" : /Linux/.test(s) ? "Linux" : "—";
|
|
208
213
|
let browser = /Edg\//.test(s) ? "Edge" : /OPR\/|Opera/.test(s) ? "Opera" : /Chrome\//.test(s) ? "Chrome" : /Firefox\//.test(s) ? "Firefox" : /Safari\//.test(s) ? "Safari" : "—";
|
|
@@ -291,15 +296,44 @@ async function setShareAuth(slug, requireAuth) {
|
|
|
291
296
|
|
|
292
297
|
// — Consultations INTERNES (équipe), table dédiée → n'affecte PAS les stats prospects. —
|
|
293
298
|
// Upsert d'une session interne (depuis l'aperçu interne de la visionneuse).
|
|
299
|
+
// ⚠️ CES CHAMPS VIENNENT DU NAVIGATEUR, DONC ILS SONT DES AFFIRMATIONS, PAS DES MESURES.
|
|
300
|
+
//
|
|
301
|
+
// La population INTERNE est celle dont le produit dit qu'elle ne doit jamais être mélangée aux
|
|
302
|
+
// prospects : « ce client a lu douze minutes » ne vaut que si un collègue relisant le document
|
|
303
|
+
// n'entre pas dans le même compte. Or n'importe qui pouvait écrire une session interne avec
|
|
304
|
+
// n'importe quel e-mail et n'importe quelle durée.
|
|
305
|
+
//
|
|
306
|
+
// Les bornes ci-dessous ne rendent pas la donnée authentique — elles empêchent qu'elle soit
|
|
307
|
+
// ABSURDE, et qu'un appel répété fasse grossir la base sans limite. L'authenticité, elle, demande
|
|
308
|
+
// que l'hôte se porte garant de l'identité (jeton signé) : c'est le second volet, et il est
|
|
309
|
+
// signalé au point d'entrée plutôt qu'ici.
|
|
310
|
+
const BORNES = { pages: 10_000, secondes: 24 * 3600, entreesPagesTime: 2_000 };
|
|
311
|
+
|
|
294
312
|
async function upsertInternalSession(p, { ip, ua }) {
|
|
295
313
|
const num = (v) => (Number.isFinite(+v) ? Math.trunc(+v) : null);
|
|
314
|
+
const borne = (v, max) => { const n = num(v); return n == null ? null : Math.max(0, Math.min(n, max)); };
|
|
296
315
|
const sessionId = String(p.sessionId || "").slice(0, 64);
|
|
297
316
|
if (!sessionId || !p.docId) return;
|
|
298
317
|
const { device, os, browser } = parseUa(ua);
|
|
299
318
|
const row = {
|
|
300
|
-
session_id: sessionId, doc_id: String(p.docId), user_email: low(p.userEmail)
|
|
301
|
-
|
|
302
|
-
|
|
319
|
+
session_id: sessionId, doc_id: String(p.docId).slice(0, 200), user_email: low(p.userEmail).slice(0, 160) || null,
|
|
320
|
+
user_name: (p.userName || "").trim().slice(0, 120) || null,
|
|
321
|
+
num_pages: borne(p.numPages, BORNES.pages), max_page: borne(p.maxPage, BORNES.pages),
|
|
322
|
+
total_seconds: borne(p.totalSeconds, BORNES.secondes) || 0,
|
|
323
|
+
// ⚠️ Un objet libre venu du dehors : sans plafond, un seul appel peut écrire un JSON de la
|
|
324
|
+
// taille qu'il veut, autant de fois qu'il veut. On garde la forme, bornée.
|
|
325
|
+
pages_time: (() => {
|
|
326
|
+
const src = (p.pagesTime && typeof p.pagesTime === "object" && !Array.isArray(p.pagesTime)) ? p.pagesTime : {};
|
|
327
|
+
const out = {};
|
|
328
|
+
let n = 0;
|
|
329
|
+
for (const k in src) {
|
|
330
|
+
if (++n > BORNES.entreesPagesTime) break;
|
|
331
|
+
const page = num(k);
|
|
332
|
+
if (page == null || page < 0 || page > BORNES.pages) continue;
|
|
333
|
+
out[String(page)] = borne(src[k], BORNES.secondes) || 0;
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
})(),
|
|
303
337
|
ua: String(ua || "").slice(0, 300), ip: String(ip || "").slice(0, 60), device, os, browser, last_at: new Date().toISOString(),
|
|
304
338
|
};
|
|
305
339
|
await PLAYER.db.request("commercial_doc_internal_sessions?on_conflict=session_id", { method: "POST", headers: { Prefer: "resolution=merge-duplicates,return=minimal" }, body: [row] });
|