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 +0 -0
- package/package.json +2 -1
- package/server/handler.js +56 -3
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.
|
|
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.
|
|
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,
|