discovery-media-player 0.1.136 → 0.1.138

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/bin/serve.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ // Copyright © 2026 3D Discovery
2
4
  // SERVEUR AUTONOME — le player sans plateforme.
3
5
  //
4
6
  // Le cœur est un gestionnaire `(req, res)` sans framework : c'est ce qui lui permet de tourner
@@ -112,8 +114,7 @@ const serveur = http.createServer(async (req, res) => {
112
114
  // Point de santé : un orchestrateur doit pouvoir savoir si le processus répond sans ouvrir un
113
115
  // document ni toucher la base.
114
116
  if (url.pathname === "/healthz") {
115
- res.writeHead(200, { "Content-Type": "application/json" });
116
- res.end(JSON.stringify({ ok: true }));
117
+ player.repondreJson(res, 200, { ok: true });
117
118
  return;
118
119
  }
119
120
 
@@ -142,8 +143,11 @@ const serveur = http.createServer(async (req, res) => {
142
143
  url.pathname === "/api/doc" || url.pathname === "/doc" || url.pathname === "/present" ||
143
144
  q.slug || q.present || q.preview || q.contract;
144
145
  if (!routeConnue) {
145
- res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
146
- res.end("Not found");
146
+ // ⚠️ LA MÊME FONCTION QUE LE PLAYER, PAS LA MÊME RECETTE ÉCRITE DEUX FOIS. Ces deux réponses
147
+ // posaient leur `Content-Type` mais PAS `nosniff` — la moitié de la règle, appliquée à la
148
+ // main, dans le seul fichier où le player ne pouvait pas la poser lui-même. Même raison que
149
+ // POLITIQUE_PERMISSIONS quelques lignes plus haut : deux exemplaires d'un fait divergent.
150
+ player.refuserEnTexte(res, 404, "Not found");
147
151
  return;
148
152
  }
149
153
 
@@ -157,8 +161,9 @@ const serveur = http.createServer(async (req, res) => {
157
161
  await player.handler(req, res);
158
162
  } catch (error) {
159
163
  console.error("[player] erreur non rattrapée", error);
160
- if (!res.headersSent) res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
161
- res.end("Erreur");
164
+ // `refuserEnTexte` porte elle-même la garde `headersSent` : une erreur survenue APRÈS le début
165
+ // de l'écriture ne peut plus rien poser, et tenter de le faire jetterait dans le rattrapage.
166
+ player.refuserEnTexte(res, 500, "Erreur");
162
167
  }
163
168
  });
164
169
 
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // CONTEXTE PAR DÉFAUT — de quoi faire tourner une instance sans écrire une ligne.
2
4
  //
3
5
  // L'architecture veut qu'un hôte écrive son câblage : c'est ce qui lui permet de brancher SON
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // D'OÙ LE PLAYER ACCEPTE DE LIRE UN FICHIER — et comment il le sert.
2
4
  //
3
5
  // Le player relaie des URL fournies par l'appelant (aperçu, démarrage de présentation). Sans
package/dist/bridge.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
+ // Copyright © 2026 3D Discovery
2
3
  //
3
4
  // ⚠️ CE FICHIER EST SOUS MIT, pas AGPL — délibérément, et c'est le seul.
4
5
  // C'est le contrat que l'application hôte doit importer pour parler au player. Le placer sous la
@@ -31,6 +31,7 @@ need.
31
31
  "product": "discovery-media-player",
32
32
  "contract": 1,
33
33
  "version": "<the running version>",
34
+ "runtime": { "node": "<what this instance runs on>", "nodeRequired": ">=22.13.0" },
34
35
  "capabilities": ["docshare", "presentations", "embed-denied", "host-fetch", "brand-reference", "host-auth", "host-share", "host-mail", "retention"],
35
36
  "frameAncestors": ["'self'", "https://*.vercel.app", "https://app.example.com"],
36
37
  "separateIssuer": true,
@@ -51,6 +52,20 @@ need.
51
52
  never by order. `plugins` lets you refuse to start when you depend on an optional module this
52
53
  instance does not have.
53
54
 
55
+ ⚠️ **`runtime` is the only way to see what the player is actually running on.** `nodeRequired` is
56
+ the floor the package declares, `node` is what the process reports — two numbers, no verdict:
57
+ compare them with your own semver rather than trusting a field we compute for you.
58
+
59
+ The floor is **Node ≥ 22.13.0**, and it is not ours: it is what `pdfjs-dist` — the one production
60
+ dependency, the one that renders your documents — requires. npm will not stop you below it. It
61
+ prints an `EBADENGINE` line in the noise of an install and installs anyway, so an instance can run
62
+ for months on a Node its rendering engine calls unsupported, and nothing says so.
63
+
64
+ ⚠️ **Read `node` rather than your platform's setting; they are not the same fact.** Measured on
65
+ 25/08 at an integrating host: the project setting said `24.x` while the deployment serving
66
+ production ran `nodejs 22`. A configured runtime is an intention, and no amount of reading it back
67
+ tells you what executed. This field is the only place the two can be confronted.
68
+
54
69
  ⚠️ **`schema` tells you which migrations this instance is still waiting for.** The player never
55
70
  applies migrations — it cannot, it only speaks PostgREST — so it *detects* instead, and a missing
56
71
  column makes the feature that needs it **degrade silently**, by design, so as not to break a host
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.136",
3
+ "version": "0.1.138",
4
4
  "description": "Self-hosted document viewer: per-recipient tracked links, reading analytics, live presentation. The core knows nothing about the application hosting it — everything it borrows arrives through an injected context.",
5
5
  "keywords": [
6
6
  "pdf-viewer",
@@ -77,7 +77,7 @@
77
77
  "test:campagne": "node -e \"require('fs').existsSync('vitest.campagne.config.mjs')||(console.error('Ce banc ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && vitest run --config vitest.campagne.config.mjs"
78
78
  },
79
79
  "engines": {
80
- "node": ">=22"
80
+ "node": ">=22.13.0"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@eslint/js": "^10.0.1",
@@ -89,6 +89,7 @@
89
89
  "fast-check": "4.9.0",
90
90
  "jsdom": "^30.0.1",
91
91
  "playwright-core": "^1.62.1",
92
+ "semver": "^7.8.5",
92
93
  "typescript": "^5.5.4",
93
94
  "typescript-eslint": "^8.46.4",
94
95
  "vitest": "^4.1.10",
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // EXTRAIT DE handler.js (refactor lot 3 — routes, 19/08/2026) — blocs déplacés À L'IDENTIQUE.
2
4
  // Reste à PLAT dans server/ (les gardes de forge ciblent server/*.js).
3
5
 
package/server/brands.js CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // Marque affichée par le loader d'un document — celle du CLIENT dont on montre le document.
2
4
  //
3
5
  // ⚠️ **Résolue à l'affichage, jamais recopiée dans le lien.** Un lien tracé vit des semaines dans
@@ -1,7 +1,9 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // GÉNÉRÉ par `npm run build:player` — NE PAS ÉDITER À LA MAIN.
2
4
  // 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
5
  //
4
6
  // Injecté par api/doc.js dans un <script nonce=…> de la visionneuse. Le code navigateur du
5
7
  // player vit en modules TypeScript testés sous player/src/ — c'est ici qu'il atterrit.
6
8
  "use strict";
7
- module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var C=Object.defineProperty,We=Object.defineProperties,Ve=Object.getOwnPropertyDescriptor,ze=Object.getOwnPropertyDescriptors,Fe=Object.getOwnPropertyNames,ve=Object.getOwnPropertySymbols;var we=Object.prototype.hasOwnProperty,Be=Object.prototype.propertyIsEnumerable;var Me=(e,n,t)=>n in e?C(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t,K=(e,n)=>{for(var t in n||(n={}))we.call(n,t)&&Me(e,t,n[t]);if(ve)for(var t of ve(n))Be.call(n,t)&&Me(e,t,n[t]);return e},xe=(e,n)=>We(e,ze(n));var M=(e,n)=>{for(var t in n)C(e,t,{get:n[t],enumerable:!0})},je=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of Fe(n))!we.call(e,o)&&o!==t&&C(e,o,{get:()=>n[o],enumerable:!(r=Ve(n,o))||r.enumerable});return e};var Ge=e=>je(C({},\"__esModule\",{value:!0}),e);var Kn={};M(Kn,{bridge:()=>N,cadence:()=>L,chat:()=>U,live:()=>H,presentation:()=>D,presentationState:()=>V,tracking:()=>$,viewer:()=>W});var N={};M(N,{onHostMessage:()=>tn,onPlayerMessage:()=>nn,parseHostMessage:()=>Te,parsePlayerMessage:()=>Ee,sendToHost:()=>Xe,sendToPlayer:()=>Je,toWire:()=>Z});var Y=\"3dd-doc-\",Ke=/^[A-Za-z0-9_-]{1,64}$/,Ye=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Ze=[\"embed-denied\"],qe=[\"present-invite\",\"present-handover\",\"present-switch\"],Qe=[\"handover-done\"];function Z(e){let n={type:Y+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(Y))return null;let o=r.slice(Y.length);if(n.includes(o))return{type:o};if(Ze.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\"||!Ke.test(s)?null:{type:o,slug:s}}function Ee(e){return _e(e,Ye,qe)}function Te(e){return _e(e,Qe,[])}function Xe(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(Z(e),\"*\")}catch(t){}}function Je(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(Z(n),\"*\")}catch(r){}}function en(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function nn(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=s=>{if(!en(t,s.source))return;let a=Ee(s.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function tn(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 $={};M($,{createTracker:()=>pn});var L={};M(L,{PRESENTER_ACTIONS_PER_HOUR:()=>X,PRESENT_CACHE_MS:()=>an,PRESENT_QUOTA_MARGIN:()=>Ne,PRESENT_QUOTA_PER_HOUR:()=>un,PRESENT_READS_PER_HOUR:()=>Ie,PRESENT_READ_BURST:()=>Ae,PRESENT_READ_COALESCE_MS:()=>Ce,PRESENT_RESYNC_MS:()=>Re,PRESENT_SIGNAL_BUDGET_PER_HOUR:()=>sn,PRESENT_WRITE_TIMEOUT_MS:()=>J,READERS_PER_EGRESS:()=>O,RESYNC_READS_PER_HOUR:()=>Pe,SESSION_IDLE_MS:()=>Q,SESSION_INTERVAL_MS:()=>q,SESSION_QUOTA_PER_HOUR:()=>rn,SESSION_WRITES_PER_HOUR:()=>Se,VIEW_EVENTS_PER_READER_HOUR:()=>ke,VIEW_QUOTA_PER_HOUR:()=>on});var q=3e4,Se=Math.ceil(120),O=25,rn=Se*O,ke=720,on=ke*O,Q=18e4,Re=25e3,Pe=Math.ceil(36e5/Re),X=720,Ie=Pe+X,sn=X,Ae=20,J=1e4,Ce=400,an=Ce,Ne=4,un=(Ie+Ae)*O*Ne;var ln=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function cn(){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 dn(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;return 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}),!0)}catch(r){return!1}}}function pn(e={}){var be,ye;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=(be=e.idleMs)!=null?be:Q,a=(ye=e.sessionEveryMs)!=null?ye:3e4,l=e.slug||\"\",p=e.internal||null,g=e.sessionId||cn(),f=e.send||dn(o,n),c={},u=0,b=0,d=0,w=0,v=null,T=r(),S=!1,z=!1,I=[],De=()=>!!l||!!p,ce=i=>{if(!De())return!1;try{return f(i)!==!1}catch(m){return!1}},F=()=>!!t&&t.visibilityState===\"visible\"&&!S,de=i=>{let m=r()-i,y=Math.max(0,T-i)+s;return Math.max(0,Math.min(m,y))},B=()=>{v!=null&&u>0&&(c[u]=(c[u]||0)+de(v)/1e3),v=F()?r():null},pe=()=>{B(),v=null},j=()=>{v==null&&F()&&(v=r())},G=()=>{let i=0;for(let m in c)i+=c[m];return v!=null&&(i+=de(v)/1e3),Math.round(i)},fe=()=>{let i={};for(let m in c)i[m]=Math.round(c[m]);return i},k=()=>{T=r(),S&&(S=!1,j())},me=(i,m)=>{p||ce({slug:l,event:i,page:m||b,maxPage:d,seconds:G(),sessionId:g})},ge=\"\",A=()=>{B();let i=G(),m=`${i}|${d}|${w}`;if(m===ge)return;let y={event:\"session\",sessionId:g,numPages:w,maxPage:d,totalSeconds:i,pagesTime:fe()};p?(y.internal=!0,y.docId=p.docId,y.email=p.email,y.name=p.name,p.it&&(y.it=p.it)):y.slug=l,ce(y)&&(ge=m)},R=(i,m,y,he)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(m,y,he),I.push(()=>{i.removeEventListener(m,y,he)}))};return{sessionId:g,totalSeconds:G,maxPage:()=>d,pageTimes:fe,start(){if(z)return;z=!0,me(\"open\",1),u=1,T=r(),v=F()?r():null,R(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(pe(),A()):(k(),j())}),R(n,\"focus\",()=>{k(),j()});for(let y of ln)R(n,y,k,{passive:!0});R(e.scrollElement,\"scroll\",k,{passive:!0}),R(n,\"beforeunload\",A);let i=n.setInterval(()=>{!S&&r()-T>s&&(S=!0,pe())},5e3),m=n.setInterval(A,a);I.push(()=>{n.clearInterval(i),n.clearInterval(m)})},setPage(i){!i||i===b||(T=r(),B(),u=i,b=i,i>d&&(d=i,me(\"page\",i)))},setPageCount(i){w=i||0},noteActivity:k,flush:A,stop(){for(;I.length;){let i=I.pop();try{i==null||i()}catch(m){}}z=!1}}}var H={};M(H,{STORAGE_KEYS:()=>x,attendeeKey:()=>bn,authorToken:()=>yn,avatarHtml:()=>ee,canModerate:()=>oe,createBudget:()=>En,createFileEcritures:()=>Tn,createScheduler:()=>xn,escapeHtml:()=>h,fetchBorne:()=>_n,flattenPresence:()=>fn,formatMessageBody:()=>ne,initials:()=>Oe,isMentioned:()=>vn,isMine:()=>_,presenceId:()=>Sn,reactorId:()=>re,referenceAuteur:()=>hn,shouldNotify:()=>Mn,unreadLabel:()=>wn});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,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\").replace(/\"/g,\"&quot;\")}function Oe(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(Oe(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 fn(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 mn(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function gn(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function bn(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 yn(e,n=te){let t=mn(e,x.authorToken);if(t)return t;let r=n();return gn(e,x.authorToken,r),r}async function hn(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 _(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 vn(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}`)&&!_(e,n):!1}function Mn(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||_(n,t)?!1:o}function wn(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function xn(e,n={}){var b;let t=(b=n.minMs)!=null?b:300,r=n.now||(()=>Date.now()),o=n.setTimer||((d,w)=>setTimeout(d,w)),s=n.clearTimer||(d=>clearTimeout(d)),a=!1,l=!1,p=-1/0,g=null,f=!1;function c(){if(f||a){l=!0;return}let d=t-(r()-p);if(d>0){u(d);return}a=!0,l=!1,p=r();let w=!1;e(()=>{w||(w=!0,a=!1,l&&!f&&c())})}function u(d){f||g!==null||(g=o(()=>{g=null,c()},Math.max(0,d)))}return{signaler(){f||c()},maintenant(){f||(p=-1/0,c())},arreter(){f=!0,g!==null&&(s(g),g=null)}}}function _n(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,b)=>setTimeout(u,b)),a=r.clearTimer||(u=>clearTimeout(u)),l=null;try{l=new AbortController}catch(u){}let p=l?xe(K({},n||{}),{signal:l.signal}):K({},n||{}),g=null,f=new Promise((u,b)=>{g=s(()=>{try{l==null||l.abort()}catch(d){}b(new Error(\"delai\"))},t)}),c=()=>{try{a(g)}catch(u){}};return Promise.race([o(e,p),f]).then(u=>(c(),u),u=>{throw c(),u})}function En(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 Tn(e={}){var g;let n=(g=e.minMs)!=null?g:0,t=e.now||(()=>Date.now()),r=e.setTimer||((f,c)=>setTimeout(f,c)),o=new Map,s=!1,a=-1/0,l=!1;function p(){if(s||o.size===0)return;let f=n-(t()-a);if(f>0){if(l)return;l=!0,r(()=>{l=!1,p()},f);return}let c=o.keys().next().value,u=o.get(c);o.delete(c),s=!0,a=t(),Promise.resolve().then(u.tache).then(b=>u.regler(b),()=>u.regler(null)).then(()=>{s=!1,p()})}return{poser(f,c){return new Promise(u=>{let b=o.get(f);o.set(f,{tache:c,regler:d=>{b&&b.regler(d),u(d)}}),p()})},enAttente:()=>o.size}}function Sn(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 U={};M(U,{messageClassName:()=>Rn,renderActions:()=>He,renderAttachment:()=>$e,renderMessage:()=>kn,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 $e(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 He(e,n){let 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 kn(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+$e(e.attachment)+`<div class=cm-re>${Le(e,n.me)}</div></span><span class=cm-act>${He(e,n)}</span>`}function Rn(e,n,t){return\"cm\"+(_(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var D={};M(D,{DEFAULT_CENTER:()=>ae,DEFAULT_ZOOM:()=>ue,MAP_TYPES:()=>P,cycleMapType:()=>Pn,initialMapContent:()=>An,mapTypeLabel:()=>In,sanitizeContent:()=>le});var P=[\"roadmap\",\"satellite\",\"hybrid\"],ae=[46.6,2.5],ue=6;function E(e){let n=Number(e);return Number.isFinite(n)?n:null}function se(e){if(!Array.isArray(e))return null;let n=E(e[0]),t=E(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:E(r.heading)||0,pitch:ie(E(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:ie(E(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:se(n.center)||[...ae],zoom:ie(Math.trunc(E(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 Pn(e){let n=P.indexOf(e);return P[(n+1)%P.length]}function In(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function An(){return{kind:\"map\",center:[...ae],zoom:ue,marker:null,mapType:null,label:null}}var W={};M(W,{CURRENT_PAGE_MARGIN:()=>Hn,DEFAULT_ASPECT:()=>On,MAX_ZOOM:()=>Nn,MIN_PAGE_WIDTH:()=>Ln,MIN_ZOOM:()=>Cn,PRERENDER_MARGIN:()=>$n,arrowState:()=>Wn,averageColor:()=>Fn,clampPage:()=>Ue,clampZoom:()=>Un,fitWidth:()=>zn,isImageDocument:()=>Bn,progressPercent:()=>Dn});var Cn=.5,Nn=3,On=1.35,Ln=280,$n=\"500px 0px\",Hn=\"-48% 0px -48% 0px\";function Un(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 Dn(e,n){return n?Math.round(Ue(e,n)/n*100):0}function Wn(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var Vn=28;function zn(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)-Vn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function Fn(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||\"\"))}var V={};M(V,{presentationTransition:()=>jn,switchDocUrl:()=>Gn});function jn(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 Gn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return Ge(Kn);})();", SOURCE_HASH: "24b1b68db3671718" };
9
+ module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var C=Object.defineProperty,We=Object.defineProperties,Ve=Object.getOwnPropertyDescriptor,ze=Object.getOwnPropertyDescriptors,Fe=Object.getOwnPropertyNames,ve=Object.getOwnPropertySymbols;var we=Object.prototype.hasOwnProperty,Be=Object.prototype.propertyIsEnumerable;var Me=(e,n,t)=>n in e?C(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t,K=(e,n)=>{for(var t in n||(n={}))we.call(n,t)&&Me(e,t,n[t]);if(ve)for(var t of ve(n))Be.call(n,t)&&Me(e,t,n[t]);return e},xe=(e,n)=>We(e,ze(n));var M=(e,n)=>{for(var t in n)C(e,t,{get:n[t],enumerable:!0})},je=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of Fe(n))!we.call(e,o)&&o!==t&&C(e,o,{get:()=>n[o],enumerable:!(r=Ve(n,o))||r.enumerable});return e};var Ge=e=>je(C({},\"__esModule\",{value:!0}),e);var Kn={};M(Kn,{bridge:()=>N,cadence:()=>L,chat:()=>U,live:()=>H,presentation:()=>D,presentationState:()=>V,tracking:()=>$,viewer:()=>W});var N={};M(N,{onHostMessage:()=>tn,onPlayerMessage:()=>nn,parseHostMessage:()=>Te,parsePlayerMessage:()=>Ee,sendToHost:()=>Xe,sendToPlayer:()=>Je,toWire:()=>Z});var Y=\"3dd-doc-\",Ke=/^[A-Za-z0-9_-]{1,64}$/,Ye=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Ze=[\"embed-denied\"],qe=[\"present-invite\",\"present-handover\",\"present-switch\"],Qe=[\"handover-done\"];function Z(e){let n={type:Y+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(Y))return null;let o=r.slice(Y.length);if(n.includes(o))return{type:o};if(Ze.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\"||!Ke.test(s)?null:{type:o,slug:s}}function Ee(e){return _e(e,Ye,qe)}function Te(e){return _e(e,Qe,[])}function Xe(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(Z(e),\"*\")}catch(t){}}function Je(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(Z(n),\"*\")}catch(r){}}function en(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function nn(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=s=>{if(!en(t,s.source))return;let a=Ee(s.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function tn(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 $={};M($,{createTracker:()=>pn});var L={};M(L,{PRESENTER_ACTIONS_PER_HOUR:()=>X,PRESENT_CACHE_MS:()=>an,PRESENT_QUOTA_MARGIN:()=>Ne,PRESENT_QUOTA_PER_HOUR:()=>un,PRESENT_READS_PER_HOUR:()=>Ie,PRESENT_READ_BURST:()=>Ae,PRESENT_READ_COALESCE_MS:()=>Ce,PRESENT_RESYNC_MS:()=>Re,PRESENT_SIGNAL_BUDGET_PER_HOUR:()=>sn,PRESENT_WRITE_TIMEOUT_MS:()=>J,READERS_PER_EGRESS:()=>O,RESYNC_READS_PER_HOUR:()=>Pe,SESSION_IDLE_MS:()=>Q,SESSION_INTERVAL_MS:()=>q,SESSION_QUOTA_PER_HOUR:()=>rn,SESSION_WRITES_PER_HOUR:()=>Se,VIEW_EVENTS_PER_READER_HOUR:()=>ke,VIEW_QUOTA_PER_HOUR:()=>on});var q=3e4,Se=Math.ceil(120),O=25,rn=Se*O,ke=720,on=ke*O,Q=18e4,Re=25e3,Pe=Math.ceil(36e5/Re),X=720,Ie=Pe+X,sn=X,Ae=20,J=1e4,Ce=400,an=Ce,Ne=4,un=(Ie+Ae)*O*Ne;var ln=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function cn(){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 dn(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;return 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}),!0)}catch(r){return!1}}}function pn(e={}){var be,ye;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=(be=e.idleMs)!=null?be:Q,a=(ye=e.sessionEveryMs)!=null?ye:3e4,l=e.slug||\"\",p=e.internal||null,g=e.sessionId||cn(),f=e.send||dn(o,n),c={},u=0,b=0,d=0,w=0,v=null,T=r(),S=!1,z=!1,I=[],De=()=>!!l||!!p,ce=i=>{if(!De())return!1;try{return f(i)!==!1}catch(m){return!1}},F=()=>!!t&&t.visibilityState===\"visible\"&&!S,de=i=>{let m=r()-i,y=Math.max(0,T-i)+s;return Math.max(0,Math.min(m,y))},B=()=>{v!=null&&u>0&&(c[u]=(c[u]||0)+de(v)/1e3),v=F()?r():null},pe=()=>{B(),v=null},j=()=>{v==null&&F()&&(v=r())},G=()=>{let i=0;for(let m in c)i+=c[m];return v!=null&&(i+=de(v)/1e3),Math.round(i)},fe=()=>{let i={};for(let m in c)i[m]=Math.round(c[m]);return i},k=()=>{T=r(),S&&(S=!1,j())},me=(i,m)=>{p||ce({slug:l,event:i,page:m||b,maxPage:d,seconds:G(),sessionId:g})},ge=\"\",A=()=>{B();let i=G(),m=`${i}|${d}|${w}`;if(m===ge)return;let y={event:\"session\",sessionId:g,numPages:w,maxPage:d,totalSeconds:i,pagesTime:fe()};p?(y.internal=!0,y.docId=p.docId,y.email=p.email,y.name=p.name,p.it&&(y.it=p.it)):y.slug=l,ce(y)&&(ge=m)},R=(i,m,y,he)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(m,y,he),I.push(()=>{i.removeEventListener(m,y,he)}))};return{sessionId:g,totalSeconds:G,maxPage:()=>d,pageTimes:fe,start(){if(z)return;z=!0,me(\"open\",1),u=1,T=r(),v=F()?r():null,R(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(pe(),A()):(k(),j())}),R(n,\"focus\",()=>{k(),j()});for(let y of ln)R(n,y,k,{passive:!0});R(e.scrollElement,\"scroll\",k,{passive:!0}),R(n,\"beforeunload\",A);let i=n.setInterval(()=>{!S&&r()-T>s&&(S=!0,pe())},5e3),m=n.setInterval(A,a);I.push(()=>{n.clearInterval(i),n.clearInterval(m)})},setPage(i){!i||i===b||(T=r(),B(),u=i,b=i,i>d&&(d=i,me(\"page\",i)))},setPageCount(i){w=i||0},noteActivity:k,flush:A,stop(){for(;I.length;){let i=I.pop();try{i==null||i()}catch(m){}}z=!1}}}var H={};M(H,{STORAGE_KEYS:()=>x,attendeeKey:()=>bn,authorToken:()=>yn,avatarHtml:()=>ee,canModerate:()=>oe,createBudget:()=>En,createFileEcritures:()=>Tn,createScheduler:()=>xn,escapeHtml:()=>h,fetchBorne:()=>_n,flattenPresence:()=>fn,formatMessageBody:()=>ne,initials:()=>Oe,isMentioned:()=>vn,isMine:()=>_,presenceId:()=>Sn,reactorId:()=>re,referenceAuteur:()=>hn,shouldNotify:()=>Mn,unreadLabel:()=>wn});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,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\").replace(/\"/g,\"&quot;\")}function Oe(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(Oe(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 fn(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 mn(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function gn(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function bn(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 yn(e,n=te){let t=mn(e,x.authorToken);if(t)return t;let r=n();return gn(e,x.authorToken,r),r}async function hn(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 _(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 vn(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}`)&&!_(e,n):!1}function Mn(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||_(n,t)?!1:o}function wn(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function xn(e,n={}){var b;let t=(b=n.minMs)!=null?b:300,r=n.now||(()=>Date.now()),o=n.setTimer||((d,w)=>setTimeout(d,w)),s=n.clearTimer||(d=>clearTimeout(d)),a=!1,l=!1,p=-1/0,g=null,f=!1;function c(){if(f||a){l=!0;return}let d=t-(r()-p);if(d>0){u(d);return}a=!0,l=!1,p=r();let w=!1;e(()=>{w||(w=!0,a=!1,l&&!f&&c())})}function u(d){f||g!==null||(g=o(()=>{g=null,c()},Math.max(0,d)))}return{signaler(){f||c()},maintenant(){f||(p=-1/0,c())},arreter(){f=!0,g!==null&&(s(g),g=null)}}}function _n(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,b)=>setTimeout(u,b)),a=r.clearTimer||(u=>clearTimeout(u)),l=null;try{l=new AbortController}catch(u){}let p=l?xe(K({},n||{}),{signal:l.signal}):K({},n||{}),g=null,f=new Promise((u,b)=>{g=s(()=>{try{l==null||l.abort()}catch(d){}b(new Error(\"delai\"))},t)}),c=()=>{try{a(g)}catch(u){}};return Promise.race([o(e,p),f]).then(u=>(c(),u),u=>{throw c(),u})}function En(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 Tn(e={}){var g;let n=(g=e.minMs)!=null?g:0,t=e.now||(()=>Date.now()),r=e.setTimer||((f,c)=>setTimeout(f,c)),o=new Map,s=!1,a=-1/0,l=!1;function p(){if(s||o.size===0)return;let f=n-(t()-a);if(f>0){if(l)return;l=!0,r(()=>{l=!1,p()},f);return}let c=o.keys().next().value,u=o.get(c);o.delete(c),s=!0,a=t(),Promise.resolve().then(u.tache).then(b=>u.regler(b),()=>u.regler(null)).then(()=>{s=!1,p()})}return{poser(f,c){return new Promise(u=>{let b=o.get(f);o.set(f,{tache:c,regler:d=>{b&&b.regler(d),u(d)}}),p()})},enAttente:()=>o.size}}function Sn(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 U={};M(U,{messageClassName:()=>Rn,renderActions:()=>He,renderAttachment:()=>$e,renderMessage:()=>kn,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 $e(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 He(e,n){let 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 kn(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+$e(e.attachment)+`<div class=cm-re>${Le(e,n.me)}</div></span><span class=cm-act>${He(e,n)}</span>`}function Rn(e,n,t){return\"cm\"+(_(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var D={};M(D,{DEFAULT_CENTER:()=>ae,DEFAULT_ZOOM:()=>ue,MAP_TYPES:()=>P,cycleMapType:()=>Pn,initialMapContent:()=>An,mapTypeLabel:()=>In,sanitizeContent:()=>le});var P=[\"roadmap\",\"satellite\",\"hybrid\"],ae=[46.6,2.5],ue=6;function E(e){let n=Number(e);return Number.isFinite(n)?n:null}function se(e){if(!Array.isArray(e))return null;let n=E(e[0]),t=E(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:E(r.heading)||0,pitch:ie(E(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:ie(E(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:se(n.center)||[...ae],zoom:ie(Math.trunc(E(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 Pn(e){let n=P.indexOf(e);return P[(n+1)%P.length]}function In(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function An(){return{kind:\"map\",center:[...ae],zoom:ue,marker:null,mapType:null,label:null}}var W={};M(W,{CURRENT_PAGE_MARGIN:()=>Hn,DEFAULT_ASPECT:()=>On,MAX_ZOOM:()=>Nn,MIN_PAGE_WIDTH:()=>Ln,MIN_ZOOM:()=>Cn,PRERENDER_MARGIN:()=>$n,arrowState:()=>Wn,averageColor:()=>Fn,clampPage:()=>Ue,clampZoom:()=>Un,fitWidth:()=>zn,isImageDocument:()=>Bn,progressPercent:()=>Dn});var Cn=.5,Nn=3,On=1.35,Ln=280,$n=\"500px 0px\",Hn=\"-48% 0px -48% 0px\";function Un(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 Dn(e,n){return n?Math.round(Ue(e,n)/n*100):0}function Wn(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var Vn=28;function zn(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)-Vn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function Fn(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||\"\"))}var V={};M(V,{presentationTransition:()=>jn,switchDocUrl:()=>Gn});function jn(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 Gn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return Ge(Kn);})();", SOURCE_HASH: "483d392a214f6bf8" };
package/server/cache.js CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // UN CACHE COURT SUR LE CHEMIN DE LECTURE PUBLIC.
2
4
  //
3
5
  // ⚠️ IL NE REMPLACE PAS UNE LIMITE, IL SUPPRIME CE QU'ELLE GARDAIT. `state=1` et `chat=1` lisent une
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // LE FAIT, PAS LE LIBELLÉ — COMMENT ON RECONNAÎT UN CONFLIT D'UNICITÉ.
2
4
  //
3
5
  // ⚠️ CE QUE FAISAIENT LES SIX SITES, ET CE QUE ÇA COÛTAIT.
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // EXTRAIT DE handler.js (refactor 19/08/2026) — texte déplacé À L'IDENTIQUE, aucun changement
2
4
  // de comportement. Reste à PLAT dans server/ : plusieurs gardes de forge ciblent server/*.js,
3
5
  // un sous-dossier les viderait en silence (la garde qui énumère maigrit quand on range).
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // EXTRAIT DE handler.js (refactor 19/08/2026) — texte déplacé À L'IDENTIQUE, aucun changement
2
4
  // de comportement. Reste à PLAT dans server/ : plusieurs gardes de forge ciblent server/*.js,
3
5
  // un sous-dossier les viderait en silence (la garde qui énumère maigrit quand on range).
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // EXTRAIT DE handler.js (refactor lot 2, 19/08/2026) — texte déplacé À L'IDENTIQUE. Reste à
2
4
  // PLAT dans server/ (les gardes de forge ciblent server/*.js).
3
5
  const { esc } = require("./texte");
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // EXTRAIT DE handler.js (refactor 19/08/2026) — texte déplacé À L'IDENTIQUE, aucun changement
2
4
  // de comportement. Reste à PLAT dans server/ : plusieurs gardes de forge ciblent server/*.js,
3
5
  // un sous-dossier les viderait en silence (la garde qui énumère maigrit quand on range).
package/server/handler.js CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // Page publique de consultation d'un document commercial : /doc/:slug → visionneuse pdf.js qui TRACE
2
4
  // l'ouverture et les PAGES VUES (un lien par destinataire → on sait qui a lu, combien de pages).
3
5
  // - GET /doc/:slug → HTML visionneuse (pdf.js EMBARQUÉ, servi par ?asset=…, nonce CSP)
@@ -50,6 +52,11 @@ let docbot = null;
50
52
  // Version publiée du player — lue là où elle est déjà déclarée, pour qu'elle ne puisse pas
51
53
  // diverger de ce que l'hôte a réellement installé.
52
54
  const PLAYER_VERSION = require("../package.json").version;
55
+ // ⚠️ LE PLANCHER, LU LÀ OÙ IL EST DÉCLARÉ. Un hôte ne peut pas confronter le Node qu'il CROIT
56
+ // déployer à celui qui TOURNE : mesuré le 25/08 chez un intégrateur dont le réglage de plateforme
57
+ // disait « 24.x » pendant que la fonction servant la production tournait en 22. Il n'avait aucune
58
+ // route à interroger — ni chez lui, ni chez nous. Le player le sait, lui ; il ne le disait pas.
59
+ const NODE_REQUIS = require("../package.json").engines.node;
53
60
  // Registre des marques : le loader porte celle du CLIENT dont on montre le document.
54
61
  const brands = require("./brands");
55
62
 
@@ -186,16 +193,10 @@ const { adresseAppelant } = require("./appelant");
186
193
  */
187
194
  const PLAFOND_RELAIS = Number(process.env.PLAYER_MAX_RELAY_BYTES || 0) || 60 * 1024 * 1024;
188
195
 
189
- // ⚠️ UN REFUS EN TEXTE POSE SON TYPE, COMME TOUT LE RESTE. Ces réponses partaient avec un code et
190
- // un corps, sans Content-Type : le seul corps de ce serveur qu'un navigateur avait le droit de
191
- // deviner, alors que la règle `nosniff` est écrite partout ailleurs. Relevé par le premier scan
192
- // ZAP baseline (règle 10019, 24/08).
193
- function refuserEnTexte(res, statut, message) {
194
- res.statusCode = statut;
195
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
196
- res.setHeader("X-Content-Type-Options", "nosniff");
197
- res.end(message);
198
- }
196
+ // ⚠️ LES PORTES DE SORTIE VIVENT DANS UN MODULE À PART, et le pourquoi est écrit là-bas : les
197
+ // familles de routes en ont besoin AUSSI, et elles ne peuvent pas recharger `handler.js` sans
198
+ // fermer un cycle. Une seule définition, requise par tout le monde.
199
+ const { refuserEnTexte, repondreJson, repondreJsonTexte } = require("./reponses.js");
199
200
 
200
201
  // ⚠️ CE QUE LES PAGES N'UTILISENT PAS EST REFUSÉ PAR ÉCRIT. Sans Permissions-Policy, un script
201
202
  // tiers compromis (le risque exact que l'inventaire TIERS épingle) hérite de tout ce que le
@@ -533,10 +534,9 @@ async function handler(req, res) {
533
534
  // rien à afficher, et ce n'est pas un refus. Le dire franchement évite qu'un intégrateur
534
535
  // cherche un lien révoqué là où il lui manque un paramètre.
535
536
  if (req.method === "GET" && !slug && !q.present && !q.preview && !q.contract && !q.asset) {
536
- res.statusCode = 400;
537
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
538
- res.end("Aucun document demandé. Attendu : ?slug=… , ?present=… , ?preview=1, ?contract=1 ou ?asset=….\n" +
539
- "Si vous intégrez le player, vérifiez que la plateforme fournit les paramètres de requête.");
537
+ refuserEnTexte(res, 400,
538
+ "Aucun document demandé. Attendu : ?slug=… , ?present=… , ?preview=1, ?contract=1 ou ?asset=….\n" +
539
+ "Si vous intégrez le player, vérifiez que la plateforme fournit les paramètres de requête.");
540
540
  return;
541
541
  }
542
542
 
@@ -590,6 +590,18 @@ async function handler(req, res) {
590
590
  // un paramètre ou un motif de refus ne le change pas.
591
591
  contract: 1,
592
592
  version: PLAYER_VERSION,
593
+ // ⚠️ SUR QUOI CETTE INSTANCE TOURNE, ET CE QU'ELLE EXIGE. Deux nombres, aucune décision :
594
+ // l'hôte compare avec son propre semver. Écrire ici « supporté : oui » demanderait un
595
+ // évaluateur d'intervalles dans le serveur, et ce dépôt a déjà payé deux fois d'avoir
596
+ // analysé un format structuré à la main.
597
+ //
598
+ // ⚠️ ON DONNE LE CORRECTIF, PAS SEULEMENT LA MINEURE, et ce n'est pas un oubli. Le plancher
599
+ // est de niveau correctif (`>=22.13.0`), donc une majeure.mineure ne permet PAS de répondre
600
+ // à la seule question posée. Et la carte publie déjà `version`, qui désigne notre code à
601
+ // l'exemplaire près : taire un chiffre de Node en publiant celui-là serait cohérent avec
602
+ // rien. La carte reste muette sur ce qui aide à ATTEIGNER l'instance — URL, secrets, noms
603
+ // d'hôte ; une version de moteur ne fait entrer personne.
604
+ runtime: { node: process.versions.node, nodeRequired: NODE_REQUIS },
593
605
  // Ce que cette instance sait faire. Un hôte teste la présence, jamais l'ordre.
594
606
  // `host-auth` : cette instance sait vérifier les jetons auprès d'un émetteur DISTINCT de
595
607
  // sa base (PLAYER_AUTH_URL). Un hôte tiers en a besoin pour savoir si ses membres
@@ -752,8 +764,7 @@ async function handler(req, res) {
752
764
  console.warn(`[player] relectures de présentation refusées : quota horaire par adresse atteint (${PRESENT_QUOTA_PER_HOUR}/h). Une audience nombreuse derrière une sortie unique verra ses pages cesser de tourner.`);
753
765
  } catch { /* sans console */ }
754
766
  }
755
- res.statusCode = 429; res.setHeader("Content-Type", "application/json"); res.setHeader("Cache-Control", "no-store");
756
- res.end(JSON.stringify({ ok: false, error: "rate" }));
767
+ repondreJson(res, 429, { ok: false, error: "rate" }, { "Cache-Control": "no-store" });
757
768
  return;
758
769
  }
759
770
  }
@@ -846,18 +857,16 @@ async function handler(req, res) {
846
857
  // 503 réessayable dit à l'appelant d'attendre une seconde ; un 500 lui dit d'abandonner.
847
858
  // Les confondre ferait renoncer une requête que le prochain battement aurait servie.
848
859
  if (e && e.code === CODE_SATURATION) {
849
- res.statusCode = 503;
850
- res.setHeader("Retry-After", String(e.retryAfter || 1));
851
- res.setHeader("Cache-Control", "no-store");
852
- res.setHeader("Content-Type", "application/json");
853
- res.end(JSON.stringify({ ok: false, error: "busy" }));
860
+ repondreJson(res, 503, { ok: false, error: "busy" },
861
+ { "Retry-After": String(e.retryAfter || 1), "Cache-Control": "no-store" });
854
862
  return;
855
863
  }
856
864
  throw e;
857
865
  }
858
866
  if (!corps) return sendRefusal(res, "ended", embed);
859
- res.statusCode = 200; res.setHeader("Content-Type", "application/json"); res.setHeader("Cache-Control", "no-store");
860
- res.end(corps);
867
+ // ⚠️ `corps` est DÉJÀ du texte JSON, construit en amont et relu à chaque battement :
868
+ // l'analyser pour le re-sérialiser serait un aller-retour pur sur un chemin chaud.
869
+ repondreJsonTexte(res, 200, corps, { "Cache-Control": "no-store" });
861
870
  return;
862
871
  }
863
872
  // ⚠️ LA LIGNE N'EST LUE QU'ICI, ET C'EST CE QUI DONNE SON SENS AU CACHE. Elle était lue
@@ -940,7 +949,7 @@ async function handler(req, res) {
940
949
  const gated = share.require_auth === true && !visitor;
941
950
 
942
951
  if (String(q.file || "") === "1") {
943
- if (gated) { res.statusCode = 401; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify({ ok: false, error: "auth" })); return; }
952
+ if (gated) { repondreJson(res, 401, { ok: false, error: "auth" }); return; }
944
953
  // Stream depuis le Storage en RELAYANT les requêtes Range → pdf.js charge progressivement (les 1res
945
954
  // pages s'affichent sans télécharger tout le PDF) → affichage bien plus rapide.
946
955
  const range = req.headers["range"];
@@ -1026,7 +1035,7 @@ async function handler(req, res) {
1026
1035
  return sendHtml(res, 200, viewerHtml(share, nonce, logoUrl, pitch), `'nonce-${nonce}'`, originesImages(logoUrl, share), frameAncestors);
1027
1036
  } catch (error) {
1028
1037
  try { await PLAYER.errors.capture(error, { route: "doc", method: req.method }); } catch { /* ignore */ }
1029
- res.statusCode = 500; res.end("Erreur");
1038
+ refuserEnTexte(res, 500, "Erreur");
1030
1039
  }
1031
1040
  }
1032
1041
 
@@ -1035,6 +1044,6 @@ async function handler(req, res) {
1035
1044
  // tenir une seconde liste — c'est la seule façon qu'une empreinte périmée finisse par se voir.
1036
1045
  // ⚠️ Exporté pour être ÉPROUVÉ, pas pour être appelé : le plafond du relais ne se vérifie qu en
1037
1046
  // regardant si le corps a été lu, ce qu aucune route ne peut montrer de l extérieur.
1038
- module.exports = { handler, init, TIERS, POLITIQUE_PERMISSIONS, __relayerFichier: relayerFichier, __jsonPourScript: jsonPourScript };
1047
+ module.exports = { handler, init, TIERS, POLITIQUE_PERMISSIONS, refuserEnTexte, repondreJson, __relayerFichier: relayerFichier, __jsonPourScript: jsonPourScript };
1039
1048
 
1040
1049
  // redeploy: forcer le build production (Vercel a sauté la prod du merge #463 — wording re-partage).
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // EXTRAIT DE handler.js (refactor lot 2, 19/08/2026) — texte déplacé À L'IDENTIQUE. Reste à
2
4
  // PLAT dans server/ (les gardes de forge ciblent server/*.js).
3
5
  const { esc, jsonPourScript } = require("./texte");
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // EXTRAIT DE handler.js (refactor lot 2, 19/08/2026) — texte déplacé À L'IDENTIQUE. Reste à
2
4
  // PLAT dans server/ (les gardes de forge ciblent server/*.js).
3
5
  const { esc, jsonPourScript } = require("./texte");
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // EXTRAIT DE handler.js (refactor lot 2, 19/08/2026) — texte déplacé À L'IDENTIQUE. Reste à
2
4
  // PLAT dans server/ (les gardes de forge ciblent server/*.js).
3
5
  const { esc, jsonPourScript } = require("./texte");
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // Mode « Présenter » : sessions de présentation live (page synchronisée). Table doc_presentations
2
4
  // (cf. migration v12324) — écriture service role only. Le présentateur détient un control_token ; on en
3
5
  // stocke le HASH (sha256) → l'audience peut lire la ligne (Realtime) sans pouvoir piloter.
package/server/publier.js CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // AUCUNE ADRESSE NE SORT — LA CATÉGORIE, PAS LES QUATRE CHEMINS CONNUS.
2
4
  //
3
5
  // Nous avions corrigé quatre chemins qui portaient l'adresse d'un membre : les champs publics du
@@ -0,0 +1,84 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
3
+ // LES PORTES PAR LESQUELLES UNE RÉPONSE QUITTE CE SERVEUR.
4
+ //
5
+ // ⚠️ POURQUOI UN MODULE, ET PAS UNE FONCTION DE PLUS DANS handler.js. `handler.js` charge les
6
+ // familles de routes ; si elles le rechargeaient pour y prendre une aide, le cycle serait fermé.
7
+ // Un module qui ne dépend de rien peut être requis par tout le monde — c'est la seule forme qui
8
+ // permet qu'il n'y ait QU'UNE définition.
9
+ //
10
+ // ⚠️ CE QUI A RENDU CE FICHIER NÉCESSAIRE, deux fois de suite.
11
+ //
12
+ // 1. LE TEXTE (audit CODEX 5.6, §3). Le `500` du bout de `/doc` posait un code et un corps et
13
+ // RIEN d'autre : le seul corps de ce serveur qu'un navigateur avait le droit de deviner. La
14
+ // règle existait pourtant depuis un mois — le premier scan ZAP baseline (règle 10019, 24/08)
15
+ // avait fait naître `refuserEnTexte`. Elle n'avait tenu que là où elle était écrite : trois
16
+ // autres réponses en texte lui avaient échappé.
17
+ //
18
+ // 2. LE JSON, trouvé en mesurant le premier. L'aide de réponse JSON était définie TREIZE fois,
19
+ // à l'identique, sous quatre noms (`jp`, `jd`, `j`, `jv`), dans quatre fichiers de routes —
20
+ // plus sept corps écrits à la main et cinq réponses inline ailleurs. Vingt endroits, et
21
+ // AUCUN ne posait `nosniff`. Ce n'est pas vingt oublis : c'est ce qu'une recette recopiée
22
+ // devient. La copie initiale était juste ; c'est la CORRECTION qui ne se propage pas.
23
+ //
24
+ // ⚠️ `nosniff` SUR DU JSON N'EST PAS DE LA SUPERSTITION. Sans lui, un navigateur garde le droit de
25
+ // requalifier un corps d'après ce qu'il contient plutôt que d'après ce qu'on annonce — et un
26
+ // contenu qui vient d'un tiers (le nom d'un document, un message de chat) voyage dans ces
27
+ // réponses. Ce dépôt écrit `nosniff` partout ailleurs depuis la 0.1.7 ; les routes API étaient le
28
+ // seul endroit où la règle s'arrêtait, parce qu'aucun scan ne les visite.
29
+
30
+ /**
31
+ * Un corps en TEXTE, avec son type et l'interdiction de le deviner.
32
+ *
33
+ * ⚠️ ELLE DOIT SURVIVRE À UN EN-TÊTE DÉJÀ PARTI. Son premier appelant est le `catch` de `/doc`, et
34
+ * une erreur peut y arriver APRÈS que la page ait commencé à s'écrire : `setHeader` jette alors
35
+ * `ERR_HTTP_HEADERS_SENT`, dans le rattrapage lui-même, ce qui transforme une erreur signalée en
36
+ * rejet non rattrapé. On ne peut plus rien poser à ce stade — le statut est parti — donc on ferme
37
+ * le flux et on se tait, ce qui est exactement ce que le client verra de toute façon. Se taire ici
38
+ * n'efface rien : l'erreur a déjà été confiée à `errors.capture`.
39
+ */
40
+ function refuserEnTexte(res, statut, message) {
41
+ if (res.headersSent) { try { res.end(); } catch { /* flux déjà clos */ } return; }
42
+ res.statusCode = statut;
43
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
44
+ res.setHeader("X-Content-Type-Options", "nosniff");
45
+ res.end(message);
46
+ }
47
+
48
+ /**
49
+ * Un corps JSON DÉJÀ SÉRIALISÉ, pour les rares appelants qui en tiennent un.
50
+ *
51
+ * ⚠️ DEUX ENTRÉES, UNE SEULE PORTE — et ce n'est pas la même chose que deux copies. L'état d'une
52
+ * présentation est construit en amont sous forme de texte JSON et relu à chaque battement : le
53
+ * ré-analyser pour le re-sérialiser serait un aller-retour pur sur un chemin chaud. Les en-têtes,
54
+ * eux, ne sont posés qu'ici — c'est le fait qui ne doit exister qu'en un exemplaire.
55
+ *
56
+ * `entetes` couvre ce qui varie légitimement d'une réponse à l'autre : `Cache-Control: no-store`
57
+ * sur un état vivant, `Retry-After` sur une saturation, `Set-Cookie` sur une entrée de visiteur.
58
+ */
59
+ function repondreJsonTexte(res, statut, texte, entetes) {
60
+ if (res.headersSent) { try { res.end(); } catch { /* flux déjà clos */ } return; }
61
+ res.statusCode = statut;
62
+ res.setHeader("Content-Type", "application/json");
63
+ res.setHeader("X-Content-Type-Options", "nosniff");
64
+ for (const [cle, valeur] of Object.entries(entetes || {})) if (valeur != null) res.setHeader(cle, valeur);
65
+ res.end(texte);
66
+ }
67
+
68
+ /** Un corps JSON à sérialiser — la forme de très loin la plus courante. */
69
+ function repondreJson(res, statut, valeur, entetes) {
70
+ repondreJsonTexte(res, statut, JSON.stringify(valeur), entetes);
71
+ }
72
+
73
+ /**
74
+ * La porte JSON LIÉE à une réponse — ce que les familles de routes tiennent en main.
75
+ *
76
+ * ⚠️ C'EST CETTE FORME QUI REMPLACE LES TREIZE COPIES, et la nuance compte. Chaque famille gardait
77
+ * une aide locale au nom court (`jp`, `jd`, `j`, `jv`) : la commodité était légitime, c'est la
78
+ * RECETTE recopiée à l'intérieur qui ne l'était pas. Le nom court reste, en une ligne qui délègue ;
79
+ * les en-têtes, eux, n'existent plus qu'en un exemplaire. Les 95 appels ne bougent pas — un
80
+ * correctif qui réécrit 95 lignes pour en corriger 13 se relit mal et se vérifie encore moins bien.
81
+ */
82
+ const jsonPour = (res) => (statut, valeur, entetes) => repondreJson(res, statut, valeur, entetes);
83
+
84
+ module.exports = { refuserEnTexte, repondreJson, repondreJsonTexte, jsonPour };
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // LA PURGE DÉCLARE, LE RECENSEMENT LA CONTREDIT.
2
4
  //
3
5
  // Ce module est UNE des deux moitiés du contrat de rétention (docs/RETENTION.md) : il efface ce
@@ -1,7 +1,16 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright © 2026 3D Discovery
1
3
  // EXTRAIT DE handler.js (refactor lot 3 — routes, 19/08/2026) — blocs déplacés À L'IDENTIQUE.
2
4
  // Reste à PLAT dans server/ (les gardes de forge ciblent server/*.js).
3
5
 
6
+ // ⚠️ EXPLICITE, PARCE QUE LE GLOBAL N'EST PAS LE MODULE. L'extraction depuis handler.js (lot 3)
7
+ // a déplacé bot-tts sans importer `crypto` : la route s'appuyait sur `globalThis.crypto`, dont la
8
+ // forme varie selon le runtime — certains y exposent `createHash`, d'autres (dont l'environnement
9
+ // des bancs) n'ont que WebCrypto, et la route rendait alors 500 à la première synthèse. Le banc
10
+ // agentVoix.test.js échoue sans cette ligne.
11
+ const crypto = require("node:crypto");
4
12
  const { adresseAppelant } = require("./appelant");
13
+ const { jsonPour } = require("./reponses.js");
5
14
 
6
15
  const { getShareBySlug } = require("./shares");
7
16
  let PLAYER = null; let docbot = null;
@@ -24,7 +33,7 @@ const ACTIONS_LIEES_A_UNE_SESSION = new Set([
24
33
 
25
34
  async function traiter(req, res, body, _slug) {
26
35
  if (body.action === "bot-tts") {
27
- const jp = (status, obj) => { res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(obj)); };
36
+ const jp = jsonPour(res);
28
37
  if (!docbot) return jp(404, { ok: false, error: "disabled" });
29
38
  try {
30
39
  const apiKey = process.env.ELEVENLABS_API_KEY;
@@ -100,10 +109,10 @@ async function traiter(req, res, body, _slug) {
100
109
  }
101
110
  } catch { /* sans alignement → synchro estimée côté client */ }
102
111
  return jp(200, { ok: true, url: pub, align: hasAlign ? pubAlign : null, spoken: spoken !== text ? spoken : undefined });
103
- } catch { return jp(500, { ok: false }); }
112
+ } catch (e) { try { PLAYER.errors.capture(e, { route: String(body.action || "(sans action)") }); } catch { /* jamais bloquant */ } return jp(500, { ok: false }); }
104
113
  }
105
114
  if (body.action === "bot-start" || body.action === "bot-say" || body.action === "bot-history" || body.action === "bot-nudge" || body.action === "bot-book" || body.action === "bot-contact" || body.action === "bot-rate" || body.action === "bot-script") {
106
- const jp = (status, obj) => { res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(obj)); };
115
+ const jp = jsonPour(res);
107
116
  if (!docbot) return jp(404, { ok: false, error: "disabled" });
108
117
  try {
109
118
  const ip = adresseAppelant(req) || "anon";
@@ -216,7 +225,7 @@ async function traiter(req, res, body, _slug) {
216
225
  const r = await docbot.botSay(String(body.sessionId || ""), share, text, pages, mobile, blang);
217
226
  if (r.error) return jp(400, { ok: false, error: r.error });
218
227
  return jp(200, { ok: true, ...r });
219
- } catch { return jp(500, { ok: false }); }
228
+ } catch (e) { try { PLAYER.errors.capture(e, { route: String(body.action || "(sans action)") }); } catch { /* jamais bloquant */ } return jp(500, { ok: false }); }
220
229
  }
221
230
  // Assistance (heartbeat) : PUBLIC (l'audience est anonyme). Journalise qui suit / combien de temps / pages vues.
222
231
  // Rate-limit généreux par IP (heartbeat ≈ 145/h/participant) : bloque le spam d'assistants factices sans