discovery-media-player 0.1.15 → 0.1.17

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
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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",
@@ -53,6 +53,7 @@
53
53
  "./package.json": "./package.json"
54
54
  },
55
55
  "scripts": {
56
+ "prepare": "node tools/install-hooks.mjs",
56
57
  "start": "node bin/serve.js",
57
58
  "build": "node build/bundle.mjs && tsc -p tsconfig.build.json && node -e \"import('./build/bundle.mjs').then(m=>m.marquerDistEsm())\"",
58
59
  "test": "vitest run",
package/server/handler.js CHANGED
@@ -1199,6 +1199,17 @@ function sendHtml(res, status, html, scriptSrc, imgExtra, frameAncestors) {
1199
1199
  `media-src 'self'${supaOrigin ? " " + supaOrigin : ""}`,
1200
1200
  "style-src 'unsafe-inline'",
1201
1201
  "base-uri 'none'",
1202
+ // ⚠️ `form-action` NE RETOMBE PAS SUR `default-src`. C'est l'une des rares directives à ne pas
1203
+ // hériter : une page en `default-src 'none'` peut malgré tout poster un formulaire n'importe
1204
+ // où. Aucune de nos pages ne contient de `<form>` — les envois passent par `fetch`, donc par
1205
+ // `connect-src` — mais un script injecté pourrait en fabriquer un pour exfiltrer vers un
1206
+ // domaine tiers, et rien ne l'en empêchait.
1207
+ //
1208
+ // `'self'` plutôt que `'none'` : le mur d'accès et la connexion visiteur peuvent avoir besoin
1209
+ // d'un envoi de même origine, et casser une authentification pour fermer une porte que
1210
+ // personne n'a franchie serait un mauvais échange. `'self'` ferme l'exfiltration, qui est le
1211
+ // risque réel.
1212
+ "form-action 'self'",
1202
1213
  // Aperçu interne : framing MÊME ORIGINE autorisé (iframe DocViewer). Page publique : 'none' (anti-clickjacking).
1203
1214
  `frame-ancestors ${frameAncestors || "'none'"}`,
1204
1215
  ].join("; "));
@@ -1220,6 +1231,17 @@ function sendSoftWallHtml(res, html, nonce, imgExtra, frameAncestors) {
1220
1231
  `img-src 'self' data: blob:${imgExtra ? " " + imgExtra : ""}`,
1221
1232
  "style-src 'unsafe-inline' https://accounts.google.com/gsi/style",
1222
1233
  "base-uri 'none'",
1234
+ // ⚠️ `form-action` NE RETOMBE PAS SUR `default-src`. C'est l'une des rares directives à ne pas
1235
+ // hériter : une page en `default-src 'none'` peut malgré tout poster un formulaire n'importe
1236
+ // où. Aucune de nos pages ne contient de `<form>` — les envois passent par `fetch`, donc par
1237
+ // `connect-src` — mais un script injecté pourrait en fabriquer un pour exfiltrer vers un
1238
+ // domaine tiers, et rien ne l'en empêchait.
1239
+ //
1240
+ // `'self'` plutôt que `'none'` : le mur d'accès et la connexion visiteur peuvent avoir besoin
1241
+ // d'un envoi de même origine, et casser une authentification pour fermer une porte que
1242
+ // personne n'a franchie serait un mauvais échange. `'self'` ferme l'exfiltration, qui est le
1243
+ // risque réel.
1244
+ "form-action 'self'",
1223
1245
  `frame-ancestors ${frameAncestors || "'self'"}`,
1224
1246
  ].join("; "));
1225
1247
  res.end(html);
@@ -1407,7 +1429,27 @@ function viewerHtml(share, nonce, logoUrl, pitch) {
1407
1429
  // INTÉGRÉ (?embed=1) : la visionneuse est en surimpression dans une page hôte de MÊME
1408
1430
  // ORIGINE (le plan d'un lot dans une expérience 3D). L'hôte n'a alors plus besoin de sa
1409
1431
  // propre barre de titre : celle-ci porte la croix, et la sortie remonte en postMessage.
1432
+ // ⚠️ DEUX QUESTIONS DIFFÉRENTES, QU'UNE SEULE VARIABLE CONFONDAIT.
1433
+ //
1434
+ // `embed` décide de l'HABILLAGE : la visionneuse porte sa propre croix, et l'hôte retire sa
1435
+ // barre de titre. L'aperçu a déjà la sienne (`closeBtn`, à droite) — lever la condition
1436
+ // afficherait donc DEUX croix. Le `!preview` n'était pas arbitraire.
1437
+ //
1438
+ // `embedded` répond à l'autre question : cette page est-elle SERVIE dans un cadre ? C'est elle
1439
+ // qui commande la poignée de main. Le serveur le sait déjà — il en tire les `frame-ancestors`
1440
+ // de la réponse — mais le navigateur ne l'apprenait qu'en aperçu de lien tracé.
1441
+ //
1442
+ // ⚠️ Conséquence signalée par le second hôte, et c'est exactement la panne que le contrat
1443
+ // décrit : `embed-ready` n'était jamais émis en aperçu, donc l'hôte recevait un SILENCE. Or le
1444
+ // silence couvre deux cas opposés — instance absente, ou player vivant. Leur guetteur de
1445
+ // démarrage a conclu « injoignable » et remplacé un player qui fonctionnait par la visionneuse
1446
+ // du navigateur, six secondes après l'ouverture, sous les yeux du lecteur. Et l'aperçu est
1447
+ // précisément le mode qu'un hôte utilise pour SES documents.
1448
+ //
1449
+ // La page était pourtant encadrable et parlait déjà (`share`, `close` en postMessage) : il ne
1450
+ // lui manquait que de dire qu'elle était là.
1410
1451
  const embed = !preview && !!share.embed;
1452
+ const embedded = !!share.embed;
1411
1453
  const fileUrl = preview ? share.stream_url : `/api/doc?slug=${encodeURIComponent(share.slug)}&file=1`;
1412
1454
  // Logo de MARQUE du loader : celui du promoteur (brand_logo, ex. MJ
1413
1455
  // Développement) s'il est renseigné, avec la mention de l'éditeur dessous (si configurée)
@@ -1421,7 +1463,7 @@ function viewerHtml(share, nonce, logoUrl, pitch) {
1421
1463
  // En aperçu interne, on embarque de quoi démarrer une présentation live (URL Storage brute + métadonnées).
1422
1464
  // `fileName` : c'est LUI qui dit la nature du document côté page. L'URL publique est
1423
1465
  // `/api/doc?slug=…&file=1`, sans extension — sans ce champ, une image partait dans pdf.js.
1424
- const cfg = JSON.stringify({ brand: PLAYER.branding.name, slug: preview ? "" : share.slug, fileUrl, fileName: share.file_name || "", pdfjs: PDFJS, title, preview, embed, bot: botOn, botGuided: !preview && !!share.bot_enabled && share.bot_guided !== false, botAv: (!preview && share.bot_enabled && share.bot_avatar) || "", botName: (!preview && share.bot_enabled && share.bot_name) || "", botGreet: (!preview && share.bot_enabled && share.bot_greeting) || "", botGreetDoc: (!preview && share.bot_enabled && share.bot_greeting_doc) || "", dl: share.allow_download !== false, autoPresent: !!share.auto_present, botAnim: share.bot_page_anim !== false, botVoice: !preview && !!share.bot_enabled && !!process.env.ELEVENLABS_API_KEY, vIcOn: ICONS.sound, vIcOff: ICONS.mute, kStyle: (!preview && share.bot_enabled && share.bot_karaoke) || "classic", vLayout: (!preview && share.bot_enabled && share.video_layout) || "", vClips: !preview && !!share.bot_vclips, botVAv: (!preview && share.bot_enabled && share.bot_vphoto) || "", resumeSlug: preview ? (share.resume_slug || "") : "", supaUrl: preview ? (share.supa_url || "") : "", supaKey: preview ? (share.supa_key || "") : "", internal: preview && share.internal_email ? { email: share.internal_email, name: share.presenter_name || "", docId: share.doc_id || "" } : null, present: preview ? { url: share.raw_url || "", name: share.file_name || "", title: share.doc_title || "", docId: share.doc_id || "", by: share.presenter_name || "", email: share.internal_email || "", av: share.presenter_avatar || "" } : null });
1466
+ const cfg = JSON.stringify({ brand: PLAYER.branding.name, slug: preview ? "" : share.slug, fileUrl, fileName: share.file_name || "", pdfjs: PDFJS, title, preview, embed, embedded, bot: botOn, botGuided: !preview && !!share.bot_enabled && share.bot_guided !== false, botAv: (!preview && share.bot_enabled && share.bot_avatar) || "", botName: (!preview && share.bot_enabled && share.bot_name) || "", botGreet: (!preview && share.bot_enabled && share.bot_greeting) || "", botGreetDoc: (!preview && share.bot_enabled && share.bot_greeting_doc) || "", dl: share.allow_download !== false, autoPresent: !!share.auto_present, botAnim: share.bot_page_anim !== false, botVoice: !preview && !!share.bot_enabled && !!process.env.ELEVENLABS_API_KEY, vIcOn: ICONS.sound, vIcOff: ICONS.mute, kStyle: (!preview && share.bot_enabled && share.bot_karaoke) || "classic", vLayout: (!preview && share.bot_enabled && share.video_layout) || "", vClips: !preview && !!share.bot_vclips, botVAv: (!preview && share.bot_enabled && share.bot_vphoto) || "", resumeSlug: preview ? (share.resume_slug || "") : "", supaUrl: preview ? (share.supa_url || "") : "", supaKey: preview ? (share.supa_key || "") : "", internal: preview && share.internal_email ? { email: share.internal_email, name: share.presenter_name || "", docId: share.doc_id || "" } : null, present: preview ? { url: share.raw_url || "", name: share.file_name || "", title: share.doc_title || "", docId: share.doc_id || "", by: share.presenter_name || "", email: share.internal_email || "", av: share.presenter_avatar || "" } : null });
1425
1467
  return `<!doctype html><html lang=fr><head><meta charset=utf-8>
1426
1468
  <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=3,viewport-fit=cover,interactive-widget=resizes-content">
1427
1469
  <meta name=robots content="noindex,nofollow">
@@ -1744,7 +1786,7 @@ ${LEGAL_CSS}
1744
1786
  // qu'on est là — sans cette poignée de main elle ne peut pas savoir si la croix
1745
1787
  // existe (visionneuse d'une version antérieure) et garde la sienne par sécurité.
1746
1788
  // Le format du fil, le targetOrigin et la validation vivent dans player/src/bridge.ts.
1747
- if(CFG.embed){
1789
+ if(CFG.embedded){
1748
1790
  Player.bridge.sendToHost({type:'embed-ready'});
1749
1791
  var _xb=document.getElementById('embedCloseBtn');
1750
1792
  if(_xb) _xb.addEventListener('click',function(){ Player.bridge.sendToHost({type:'close'}); });
@@ -1943,6 +1985,17 @@ function sendPresentHtml(res, html, nonce, supaUrl, imgExtra, frameAncestors) {
1943
1985
  "style-src 'unsafe-inline' https://unpkg.com https://fonts.googleapis.com",
1944
1986
  "font-src https://fonts.gstatic.com data:",
1945
1987
  "base-uri 'none'",
1988
+ // ⚠️ `form-action` NE RETOMBE PAS SUR `default-src`. C'est l'une des rares directives à ne pas
1989
+ // hériter : une page en `default-src 'none'` peut malgré tout poster un formulaire n'importe
1990
+ // où. Aucune de nos pages ne contient de `<form>` — les envois passent par `fetch`, donc par
1991
+ // `connect-src` — mais un script injecté pourrait en fabriquer un pour exfiltrer vers un
1992
+ // domaine tiers, et rien ne l'en empêchait.
1993
+ //
1994
+ // `'self'` plutôt que `'none'` : le mur d'accès et la connexion visiteur peuvent avoir besoin
1995
+ // d'un envoi de même origine, et casser une authentification pour fermer une porte que
1996
+ // personne n'a franchie serait un mauvais échange. `'self'` ferme l'exfiltration, qui est le
1997
+ // risque réel.
1998
+ "form-action 'self'",
1946
1999
  `frame-ancestors ${frameAncestors || "'none'"}`,
1947
2000
  ].join("; "));
1948
2001
  res.end(html);
@@ -2810,7 +2863,7 @@ async function handler(req, res) {
2810
2863
  }
2811
2864
  const supaUrl = (PLAYER.config && PLAYER.config.supabaseUrl) || "";
2812
2865
  const supaKey = (PLAYER.config && PLAYER.config.supabasePublishableKey) || "";
2813
- const pseudo = { preview: true, slug: "", file_name: String(q.name || "document.pdf"), doc_title: String(q.title || q.name || "Document"), raw_url: url, doc_id: String(q.docId || ""), presenter_name: String(q.by || ""), presenter_avatar: String(q.av || ""), internal_email: String(q.uemail || ""), supa_url: supaUrl, supa_key: supaKey, auto_present: String(q.autopresent || "") === "1", resume_slug: String(q.resume || ""), stream_url: `/api/doc?preview=1&stream=1&url=${encodeURIComponent(url)}&name=${encodeURIComponent(String(q.name || ""))}` };
2866
+ const pseudo = { preview: true, embed, slug: "", file_name: String(q.name || "document.pdf"), doc_title: String(q.title || q.name || "Document"), raw_url: url, doc_id: String(q.docId || ""), presenter_name: String(q.by || ""), presenter_avatar: String(q.av || ""), internal_email: String(q.uemail || ""), supa_url: supaUrl, supa_key: supaKey, auto_present: String(q.autopresent || "") === "1", resume_slug: String(q.resume || ""), stream_url: `/api/doc?preview=1&stream=1&url=${encodeURIComponent(url)}&name=${encodeURIComponent(String(q.name || ""))}` };
2814
2867
  let plogo = ""; try { plogo = await PLAYER.branding.logo(); } catch { /* sans logo */ }
2815
2868
  const pnonce = crypto.randomBytes(16).toString("base64");
2816
2869
  // Aperçu interne : CSP relâchée (supabase-js jsdelivr + Realtime wss) pour la présence + le chat live,