discovery-media-player 0.1.0

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.
@@ -0,0 +1,230 @@
1
+ // D'OÙ LE PLAYER ACCEPTE DE LIRE UN FICHIER — et comment il le sert.
2
+ //
3
+ // Le player relaie des URL fournies par l'appelant (aperçu, démarrage de présentation). Sans
4
+ // barrière, il deviendrait un proxy universel capable d'atteindre le réseau interne de son
5
+ // hébergeur. C'est le fichier le plus sensible du projet : **refus par défaut**, trois sources
6
+ // autorisées explicitement, rien d'autre.
7
+ //
8
+ // Ce module ne dépend d'aucun hôte : il est partagé par le studio historique et par le serveur
9
+ // autonome (`bin/serve.js`). Une seule implémentation de la garde — c'est la seule façon qu'un
10
+ // durcissement profite aux deux.
11
+
12
+ const fs = require("node:fs");
13
+ const fsp = require("node:fs/promises");
14
+ const path = require("node:path");
15
+
16
+ /** Chemin canonique des objets PUBLICS du Storage Supabase. */
17
+ const PUBLIC_OBJECT_PREFIX = "/storage/v1/object/public/";
18
+
19
+ /**
20
+ * Origines de Storage autorisées : celle de la base, plus `PLAYER_STORAGE_ORIGINS`
21
+ * (séparées par espace ou virgule).
22
+ */
23
+ function storageOrigins(env) {
24
+ const raw = [env.SUPABASE_URL, ...String(env.PLAYER_STORAGE_ORIGINS || "").split(/[\s,]+/)];
25
+ const origins = [];
26
+ for (const candidate of raw) {
27
+ if (!candidate) continue;
28
+ try {
29
+ const { origin } = new URL(candidate);
30
+ if (origin && origin !== "null" && !origins.includes(origin)) origins.push(origin);
31
+ } catch { /* entrée illisible : ignorée, jamais fatale */ }
32
+ }
33
+ return origins;
34
+ }
35
+
36
+ /**
37
+ * Route de l'hôte qui sert les fichiers — motif « l'hôte sert le fichier ».
38
+ *
39
+ * Tous les hôtes n'ont pas leurs documents dans un Storage joignable : ils vivent souvent derrière
40
+ * une clé d'API tierce. Le player **ne portera jamais les identifiants d'un tiers** — autoriser
41
+ * l'origine du tiers lui donnerait le droit d'y lire n'importe quoi avec ces identifiants. L'hôte
42
+ * expose donc UNE route, va chercher le document lui-même, et le player n'autorise que celle-là.
43
+ *
44
+ * `PLAYER_HOST_FETCH_BASE` est un préfixe d'URL COMPLET (origine + début de chemin), jamais une
45
+ * simple origine : autoriser tout un domaine applicatif rendrait le player capable d'appeler
46
+ * n'importe quelle route de l'hôte.
47
+ */
48
+ function hostFetchBase(env) {
49
+ try {
50
+ const raw = String(env.PLAYER_HOST_FETCH_BASE || "");
51
+ if (!raw) return null;
52
+ const url = new URL(raw);
53
+ if (url.protocol !== "https:") return null;
54
+ // ⚠️ LA BARRE FINALE EST OBLIGATOIRE, et on la remet plutôt que d'espérer qu'elle soit tapée.
55
+ // La comparaison est un préfixe de CHAÎNE : `…/api/documents` saisi sans barre ouvrirait aussi
56
+ // `/api/documents-prives/tout`. Aucune erreur nulle part — ça marche, ça couvre simplement plus
57
+ // large que ce que quiconque a voulu. Et la faute est du côté le plus facile à commettre : une
58
+ // variable d'environnement tapée à la main, un jour de déploiement.
59
+ const p = url.pathname.endsWith("/") ? url.pathname : url.pathname + "/";
60
+ return { origin: url.origin, path: p };
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /** L'URL désigne-t-elle la route de fichiers de l'hôte ? */
67
+ function isHostFetchUrl(candidate, base) {
68
+ if (!base) return false;
69
+ try {
70
+ const url = new URL(String(candidate || ""));
71
+ if (url.protocol !== "https:" || url.username || url.password) return false;
72
+ return url.origin === base.origin && url.pathname.startsWith(base.path);
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ // ── Fichiers locaux ────────────────────────────────────────────────────────────────────────────
79
+ // Pour servir un dossier de documents sans base ni Storage : `PLAYER_LOCAL_ROOT=/data` autorise
80
+ // les URL `file:` situées SOUS ce dossier. C'est ce qui rend le projet essayable en deux commandes,
81
+ // et c'est un vrai mode d'exploitation pour qui héberge chez lui.
82
+ //
83
+ // ⚠️ Délibérément un CHEMIN, pas une origine réseau. On aurait pu autoriser `http://localhost` :
84
+ // ç'aurait été rouvrir exactement la porte que la garde ferme (le réseau interne de l'hébergeur
85
+ // commence à `localhost`). Un dossier ne mène nulle part ailleurs qu'à lui-même.
86
+
87
+ function localRoot(env) {
88
+ const raw = String(env.PLAYER_LOCAL_ROOT || "").trim();
89
+ if (!raw) return null;
90
+ try { return fs.realpathSync(path.resolve(raw)); } catch { return null; }
91
+ }
92
+
93
+ /**
94
+ * Le chemin visé reste-t-il SOUS la racine ?
95
+ *
96
+ * ⚠️ On compare des chemins RÉSOLUS et on exige le séparateur : sans lui, une racine `/data`
97
+ * accepterait `/data-prive`. C'est le même piège que la barre finale de `PLAYER_HOST_FETCH_BASE`,
98
+ * une couche plus bas — préfixe de chaîne contre préfixe de segment.
99
+ */
100
+ function resolveLocal(candidate, root) {
101
+ if (!root) return null;
102
+ try {
103
+ const url = new URL(String(candidate || ""));
104
+ if (url.protocol !== "file:") return null;
105
+ // `decodeURIComponent` + `path.resolve` normalisent les `..` avant la comparaison ; le `%00`
106
+ // est refusé net (un octet nul tronque un chemin dans certaines couches basses).
107
+ const brut = decodeURIComponent(url.pathname);
108
+ if (brut.includes("\0")) return null;
109
+ // ⚠️ On compare des chemins RÉELS des deux côtés. `path.resolve` seul suffirait contre les
110
+ // `..` (déjà normalisés), mais pas contre un lien symbolique posé DANS la racine et pointant
111
+ // dehors — et pas non plus contre une racine qui EST un lien (sur macOS, `/tmp` mène à
112
+ // `/private/tmp` : la garde refusait alors tout, ce qui se voit, mais l'inverse ne se verrait
113
+ // pas). Si le fichier n'existe pas, le chemin normalisé suffit : il finira en 404.
114
+ let cible = path.resolve(brut);
115
+ try { cible = fs.realpathSync(cible); } catch { /* inexistant : la forme normalisée fait foi */ }
116
+ if (cible !== root && !cible.startsWith(root + path.sep)) return null;
117
+ return cible;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ const TYPES = {
124
+ ".pdf": "application/pdf", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
125
+ ".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml", ".avif": "image/avif",
126
+ };
127
+
128
+ /**
129
+ * Lit un fichier local en présentant la MÊME forme qu'une réponse `fetch` : le reste du player ne
130
+ * doit pas savoir d'où vient l'octet. Le `Range` est honoré — c'est de lui que vient l'affichage
131
+ * des premières pages avant la fin du téléchargement.
132
+ */
133
+ async function readLocal(cible, range) {
134
+ let stat;
135
+ try { stat = await fsp.stat(cible); } catch { return null; }
136
+ if (!stat.isFile()) return null;
137
+
138
+ const type = TYPES[path.extname(cible).toLowerCase()] || "application/octet-stream";
139
+ const total = stat.size;
140
+ const m = /^bytes=(\d*)-(\d*)$/.exec(String(range || ""));
141
+ let debut = 0, fin = total - 1, statut = 200;
142
+ if (m && (m[1] || m[2])) {
143
+ if (m[1]) { debut = Number(m[1]); fin = m[2] ? Math.min(Number(m[2]), total - 1) : total - 1; }
144
+ else { debut = Math.max(0, total - Number(m[2])); } // `bytes=-500` : les 500 derniers
145
+ if (!(debut >= 0 && fin >= debut && debut < total)) {
146
+ return reponse(416, {}, Buffer.alloc(0)); // borne absurde : on le dit
147
+ }
148
+ statut = 206;
149
+ }
150
+
151
+ const fh = await fsp.open(cible, "r");
152
+ try {
153
+ const buf = Buffer.alloc(fin - debut + 1);
154
+ const { bytesRead } = await fh.read(buf, 0, buf.length, debut);
155
+ const corps = bytesRead === buf.length ? buf : buf.subarray(0, bytesRead);
156
+ const entetes = { "content-type": type };
157
+ if (statut === 206) entetes["content-range"] = `bytes ${debut}-${fin}/${total}`;
158
+ return reponse(statut, entetes, corps);
159
+ } finally {
160
+ await fh.close();
161
+ }
162
+ }
163
+
164
+ function reponse(status, entetes, corps) {
165
+ const h = new Map(Object.entries(entetes));
166
+ return {
167
+ ok: status < 400,
168
+ status,
169
+ headers: { get: (k) => (h.has(String(k).toLowerCase()) ? h.get(String(k).toLowerCase()) : null) },
170
+ arrayBuffer: async () => corps,
171
+ };
172
+ }
173
+
174
+ // ── La garde ───────────────────────────────────────────────────────────────────────────────────
175
+
176
+ /**
177
+ * Refus par défaut. On n'accepte que :
178
+ * - la route de fichiers de l'hôte (préfixe complet, https) ;
179
+ * - un objet PUBLIC d'un Storage explicitement autorisé (ces fichiers sont déjà accessibles à
180
+ * tous : on n'expose donc rien de neuf) ;
181
+ * - un fichier sous `PLAYER_LOCAL_ROOT`, si cette racine est configurée.
182
+ *
183
+ * Jamais d'identifiants dans l'URL (`https://victime@vrai-hote/…` : l'origine est bonne, mais des
184
+ * identifiants voyageraient avec la requête).
185
+ */
186
+ function isAllowedStorageUrl(candidate, origins, hostBase, root) {
187
+ if (isHostFetchUrl(candidate, hostBase)) return true;
188
+ if (root && resolveLocal(candidate, root)) return true;
189
+ try {
190
+ const url = new URL(String(candidate || ""));
191
+ if (url.protocol !== "https:") return false;
192
+ if (url.username || url.password) return false;
193
+ if (!origins.includes(url.origin)) return false;
194
+ // `pathname` est déjà normalisé par URL (les « .. » sont résolus avant d'arriver ici).
195
+ return url.pathname.startsWith(PUBLIC_OBJECT_PREFIX);
196
+ } catch {
197
+ return false;
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Va chercher un fichier autorisé, en relayant la requête `Range`.
203
+ *
204
+ * ⚠️ On demande explicitement du NON COMPRESSÉ : `fetch()` décompresse le corps tout en gardant
205
+ * les en-têtes de l'amont — relayer sa taille annoncée servirait un PDF tronqué, sans erreur.
206
+ *
207
+ * ⚠️ Le secret n'accompagne QUE la route de l'hôte. L'envoyer à un Storage public le ferait fuiter
208
+ * dans les journaux d'un tiers, où il n'a rien à faire.
209
+ */
210
+ async function fetchAllowedFile(url, { range } = {}, { origins, hostBase, root, secret } = {}) {
211
+ if (!isAllowedStorageUrl(url, origins || [], hostBase, root)) return null;
212
+ const local = root && resolveLocal(url, root);
213
+ if (local) return readLocal(local, range);
214
+
215
+ const headers = { "accept-encoding": "identity" };
216
+ if (range) headers.range = range;
217
+ if (isHostFetchUrl(url, hostBase) && secret) headers["x-player-fetch-secret"] = secret;
218
+ return fetch(url, { headers });
219
+ }
220
+
221
+ module.exports = {
222
+ PUBLIC_OBJECT_PREFIX,
223
+ storageOrigins,
224
+ hostFetchBase,
225
+ isHostFetchUrl,
226
+ localRoot,
227
+ resolveLocal,
228
+ isAllowedStorageUrl,
229
+ fetchAllowedFile,
230
+ };
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "discovery-media-player",
3
+ "version": "0.1.0",
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
+ "keywords": [
6
+ "pdf-viewer",
7
+ "document-tracking",
8
+ "read-receipts",
9
+ "document-analytics",
10
+ "live-presentation",
11
+ "self-hosted",
12
+ "pdfjs",
13
+ "sales-enablement"
14
+ ],
15
+ "license": "AGPL-3.0-or-later",
16
+ "author": "3D Discovery",
17
+ "homepage": "https://github.com/Juli1artha/discovery-media-player#readme",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/Juli1artha/discovery-media-player.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/Juli1artha/discovery-media-player/issues"
24
+ },
25
+ "main": "server/handler.js",
26
+ "bin": {
27
+ "discovery-media-player": "bin/serve.js"
28
+ },
29
+ "files": [
30
+ "bin",
31
+ "context",
32
+ "server",
33
+ "src",
34
+ "supabase",
35
+ "README.md",
36
+ "CONTRAT.md",
37
+ "LICENSE",
38
+ "LICENSE-MIT"
39
+ ],
40
+ "exports": {
41
+ ".": "./server/handler.js",
42
+ "./shares": "./server/shares.js",
43
+ "./presentations": "./server/presentations.js",
44
+ "./brands": "./server/brands.js",
45
+ "./context/standalone": "./context/standalone.js",
46
+ "./context/storage": "./context/storage.js",
47
+ "./bridge": "./src/bridge.ts",
48
+ "./package.json": "./package.json"
49
+ },
50
+ "scripts": {
51
+ "start": "node bin/serve.js",
52
+ "build": "node build/bundle.mjs",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "lint": "eslint bin context server src build",
56
+ "lint:fix": "eslint bin context server src build --fix",
57
+ "typecheck": "tsc --noEmit",
58
+ "prepublishOnly": "npm run build && npm test"
59
+ },
60
+ "engines": {
61
+ "node": ">=18"
62
+ },
63
+ "devDependencies": {
64
+ "@eslint/js": "^9.39.4",
65
+ "esbuild": "0.21.5",
66
+ "eslint": "^9.39.4",
67
+ "jsdom": "^25.0.1",
68
+ "typescript": "^5.5.4",
69
+ "typescript-eslint": "^8.46.4",
70
+ "vitest": "^2.1.9"
71
+ }
72
+ }
@@ -0,0 +1,44 @@
1
+ // Marque affichée par le loader d'un document — celle du CLIENT dont on montre le document.
2
+ //
3
+ // ⚠️ **Résolue à l'affichage, jamais recopiée dans le lien.** Un lien tracé vit des semaines dans
4
+ // une boîte mail : un logo figé au moment de l'envoi ne suivrait pas une charte corrigée.
5
+ //
6
+ // ⚠️ Et **le player ne tient pas de registre**. Une première version en avait un — table dédiée,
7
+ // actions d'administration — jusqu'à ce qu'on constate que l'hôte a DÉJÀ cette donnée (chez le
8
+ // studio : le logo de l'espace client, sur la fiche société du CRM). Un registre en aurait été le
9
+ // miroir, donc une chose de plus à synchroniser : exactement le problème qu'on cherchait à éviter,
10
+ // un étage plus haut. Le lien porte une CLÉ, l'hôte répond.
11
+ //
12
+ // Le player ne sait pas ce qu'est un client : il ne connaît que des clés.
13
+
14
+ // ⚠️ Le contexte est REÇU, pas construit. Ce module ne doit pas savoir d'où il vient : c'est ce
15
+ // qui lui permettra de partir dans le dépôt du player sans emporter le studio avec lui.
16
+ let PLAYER = null;
17
+ function init(ctx) { PLAYER = ctx; }
18
+
19
+
20
+ /**
21
+ * Marque à afficher pour un partage, par ordre de priorité :
22
+ * 1. la clé du lien (`brand_key`), résolue MAINTENANT par l'hôte — donc toujours à jour ;
23
+ * 2. le logo recopié dans le lien (`brand_logo`) — flux historiques, toujours honoré ;
24
+ * 3. rien : le loader affiche la marque de l'instance.
25
+ *
26
+ * Ne lève jamais : une marque introuvable dégrade le loader, elle n'empêche pas de lire le document.
27
+ */
28
+ async function brandForShare(share) {
29
+ if (!share) return null;
30
+ if (share.brand_key) {
31
+ // Le commentaire ci-dessus dit « ne lève jamais » : on le rend vrai ici aussi, sans compter
32
+ // sur la garde du contexte. Une marque introuvable dégrade le loader, elle ne casse rien.
33
+ try {
34
+ const brand = await PLAYER.branding.forKey(share.brand_key);
35
+ if (brand) return brand;
36
+ } catch { /* on retombe sur le logo du lien */ }
37
+ }
38
+ // Repli historique : un lien créé avant les clés porte son logo recopié. Pas de nom à ce
39
+ // stade — ces liens n'en ont jamais transporté.
40
+ if (share.brand_logo) return { logo: String(share.brand_logo), name: "", dark: !!share.brand_dark };
41
+ return null;
42
+ }
43
+
44
+ module.exports = { init, brandForShare };
@@ -0,0 +1,7 @@
1
+ // GÉNÉRÉ par `npm run build:player` — NE PAS ÉDITER À LA MAIN.
2
+ // Sources : src/index.ts, src/bridge.ts, src/tracking.ts, src/live.ts, src/chat.ts, src/presentation-content.ts, src/viewer.ts, src/presentation-state.ts
3
+ //
4
+ // Injecté par api/doc.js dans un <script nonce=…> de la visionneuse. Le code navigateur du
5
+ // player vit en modules TypeScript testés sous player/src/ — c'est ici qu'il atterrit.
6
+ "use strict";
7
+ module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var U=Object.defineProperty;var ye=Object.getOwnPropertyDescriptor;var ve=Object.getOwnPropertyNames;var Me=Object.prototype.hasOwnProperty;var d=(e,n)=>{for(var t in n)U(e,t,{get:n[t],enumerable:!0})},xe=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of ve(n))!Me.call(e,o)&&o!==t&&U(e,o,{get:()=>n[o],enumerable:!(r=ye(n,o))||r.enumerable});return e};var we=e=>xe(U({},\"__esModule\",{value:!0}),e);var dn={};d(dn,{bridge:()=>E,chat:()=>I,live:()=>A,presentation:()=>C,presentationState:()=>$,tracking:()=>S,viewer:()=>L});var E={};d(E,{onHostMessage:()=>Ce,onPlayerMessage:()=>Ie,parseHostMessage:()=>ce,parsePlayerMessage:()=>ue,sendToHost:()=>Se,sendToPlayer:()=>Ae,toWire:()=>K});var V=\"3dd-doc-\",ke=/^[A-Za-z0-9_-]{1,64}$/,Te=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],_e=[\"embed-denied\"],Pe=[\"present-invite\",\"present-handover\",\"present-switch\"],Ee=[\"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 le(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(_e.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\"||!ke.test(i)?null:{type:o,slug:i}}function ue(e){return le(e,Te,Pe)}function ce(e){return le(e,Ee,[])}function Se(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(K(e),\"*\")}catch(t){}}function Ae(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(K(n),\"*\")}catch(r){}}function Ie(e,n){let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=o=>{let i=ue(o.data);i&&e(i)};return t.addEventListener(\"message\",r),()=>t.removeEventListener(\"message\",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)}var S={};d(S,{createTracker:()=>He});var Le=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function $e(){return Math.random().toString(36).slice(2)+Date.now().toString(36)}function Re(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 He(e={}){var se,ie;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=(se=e.idleMs)!=null?se:6e4,u=(ie=e.sessionEveryMs)!=null?ie:12e3,h=e.slug||\"\",p=e.internal||null,R=e.sessionId||$e(),be=e.send||Re(o,n),g={},y=0,H=0,v=0,ee=0,c=null,N=r(),M=!1,O=!1,T=[],he=()=>!!h||!!p,ne=s=>{if(he())try{be(s)}catch(a){}},te=()=>!!t&&t.visibilityState===\"visible\"&&(typeof t.hasFocus==\"function\"?t.hasFocus():!0)&&!M,W=()=>{c!=null&&y>0&&(g[y]=(g[y]||0)+(r()-c)/1e3),c=te()?r():null},D=()=>{W(),c=null},F=()=>{c==null&&te()&&(c=r())},z=()=>{let s=0;for(let a in g)s+=g[a];return c!=null&&(s+=(r()-c)/1e3),Math.round(s)},re=()=>{let s={};for(let a in g)s[a]=Math.round(g[a]);return s},x=()=>{N=r(),M&&(M=!1,F())},oe=(s,a)=>{p||ne({slug:h,event:s,page:a||H,maxPage:v,seconds:z(),sessionId:R})},_=()=>{W();let s={event:\"session\",sessionId:R,numPages:ee,maxPage:v,totalSeconds:z(),pagesTime:re()};p?(s.internal=!0,s.docId=p.docId,s.email=p.email,s.name=p.name):s.slug=h,ne(s)},f=(s,a,P,ae)=>{!s||typeof s.addEventListener!=\"function\"||(s.addEventListener(a,P,ae),T.push(()=>{s.removeEventListener(a,P,ae)}))};return{sessionId:R,totalSeconds:z,maxPage:()=>v,pageTimes:re,start(){if(O)return;O=!0,oe(\"open\",1),y=1,c=r(),N=r(),f(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(D(),_()):(x(),F())}),f(n,\"blur\",D),f(n,\"focus\",()=>{x(),F()});for(let P of Le)f(n,P,x,{passive:!0});f(e.scrollElement,\"scroll\",x,{passive:!0}),f(n,\"beforeunload\",_);let s=n.setInterval(()=>{!M&&r()-N>i&&(M=!0,D())},5e3),a=n.setInterval(_,u);T.push(()=>{n.clearInterval(s),n.clearInterval(a)})},setPage(s){!s||s===H||(W(),y=s,H=s,s>v&&(v=s,oe(\"page\",s)))},setPageCount(s){ee=s||0},noteActivity:x,flush:_,stop(){for(;T.length;){let s=T.pop();try{s==null||s()}catch(a){}}O=!1}}}var A={};d(A,{STORAGE_KEYS:()=>w,attendeeKey:()=>De,authorToken:()=>Fe,avatarHtml:()=>G,canModerate:()=>Z,escapeHtml:()=>l,flattenPresence:()=>Ne,formatMessageBody:()=>j,initials:()=>de,isMentioned:()=>ze,isMine:()=>m,reactorId:()=>B,shouldNotify:()=>Ue,unreadLabel:()=>Ve});var w={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\"};function l(e){return String(e==null?\"\":e).replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\").replace(/\"/g,\"&quot;\")}function de(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(de(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 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 We(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function De(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(w.attendeeKey);if(o)return o;let i=`anon-${r()}`;return n.setItem(w.attendeeKey,i),i}catch(o){return`anon-${t}`}}function Fe(e,n=()=>Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)){let t=Oe(e,w.authorToken);if(t)return t;let r=n();return We(e,w.authorToken,r),r}function B(e){return String(e&&(e.email||e.name)||\"\").toLowerCase()}function m(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 ze(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}`)&&!m(e,n):!1}function Ue(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||m(n,t)?!1:o}function Ve(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}var I={};d(I,{messageClassName:()=>Ge,renderActions:()=>fe,renderAttachment:()=>ge,renderMessage:()=>Ke,renderReactions:()=>pe});function pe(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 h=u.indexOf(r)>=0;o+=`<button class=\"re-chip${h?\" 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 fe(e,n){let t=m(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 Ke(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>${pe(e,n.me)}</div></span><span class=cm-act>${fe(e,n)}</span>`}function Ge(e,n,t){return\"cm\"+(m(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var C={};d(C,{DEFAULT_CENTER:()=>X,DEFAULT_ZOOM:()=>J,MAP_TYPES:()=>k,cycleMapType:()=>je,initialMapContent:()=>Ze,mapTypeLabel:()=>Be,sanitizeContent:()=>Q});var k=[\"roadmap\",\"satellite\",\"hybrid\"],X=[46.6,2.5],J=6;function b(e){let n=Number(e);return Number.isFinite(n)?n:null}function Y(e){if(!Array.isArray(e))return null;let n=b(e[0]),t=b(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:b(r.heading)||0,pitch:q(b(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:q(b(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:Y(n.center)||[...X],zoom:q(Math.trunc(b(n.zoom)||J),1,21),marker:Y(n.marker),mapType:k.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function je(e){let n=k.indexOf(e);return k[(n+1)%k.length]}function Be(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function Ze(){return{kind:\"map\",center:[...X],zoom:J,marker:null,mapType:null,label:null}}var L={};d(L,{CURRENT_PAGE_MARGIN:()=>en,DEFAULT_ASPECT:()=>Xe,MAX_ZOOM:()=>qe,MIN_PAGE_WIDTH:()=>Je,MIN_ZOOM:()=>Ye,PRERENDER_MARGIN:()=>Qe,arrowState:()=>rn,averageColor:()=>an,clampPage:()=>me,clampZoom:()=>nn,fitWidth:()=>sn,isImageDocument:()=>ln,progressPercent:()=>tn});var Ye=.5,qe=3,Xe=1.35,Je=280,Qe=\"500px 0px\",en=\"-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 me(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function tn(e,n){return n?Math.round(me(e,n)/n*100):0}function rn(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var on=28;function sn(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)-on-(e.overlap||0)-(e.reserve||0))/r;return i>120?Math.min(t,i):t}function an(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 ln(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var $={};d($,{presentationTransition:()=>un,switchDocUrl:()=>cn});function un(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 cn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return we(dn);})();", SOURCE_HASH: "dbdf24ce25f84ac7" };