discovery-media-player 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/context/standalone.js +35 -1
- package/package.json +2 -2
- package/server/handler.js +16 -9
- package/server/presentations.js +18 -8
package/context/standalone.js
CHANGED
|
@@ -164,6 +164,39 @@ function createStandaloneContext(env = process.env) {
|
|
|
164
164
|
}),
|
|
165
165
|
// Écriture de fichier : hors périmètre d'une instance autonome (elle sert, elle ne range pas).
|
|
166
166
|
async put() { throw new Error("storage.put n'est pas disponible sans câblage d'hôte"); },
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* URL d'envoi signée, pour une pièce jointe de chat. **Capacité de l'HÔTE**, pas du cœur.
|
|
170
|
+
*
|
|
171
|
+
* ⚠️ Le cœur lisait `SUPABASE_SERVICE_ROLE_KEY` directement dans l'environnement — la clé
|
|
172
|
+
* qui ouvre toute la base, lue en contournant le contexte injecté, c'est-à-dire en violant
|
|
173
|
+
* la règle que le projet affiche partout ailleurs. Un hôte qui branche SA base se faisait
|
|
174
|
+
* court-circuiter ici sans que rien ne le signale.
|
|
175
|
+
*
|
|
176
|
+
* ⚠️ Honnête sur ce qui reste : la page renvoyée appelle `uploadToSignedUrl` de supabase-js.
|
|
177
|
+
* Cette capacité retire le secret du cœur ; elle ne rend pas la fonction portable pour
|
|
178
|
+
* autant. Un hôte sur une autre pile devra aussi remplacer la moitié navigateur.
|
|
179
|
+
*/
|
|
180
|
+
async signUpload(bucket, chemin) {
|
|
181
|
+
const base = sansBarreFinale(env.SUPABASE_URL);
|
|
182
|
+
const cle = String(env.SUPABASE_SERVICE_ROLE_KEY || "");
|
|
183
|
+
if (!base || !cle || !bucket || !chemin) return null;
|
|
184
|
+
try {
|
|
185
|
+
const r = await fetch(`${base}/storage/v1/object/upload/sign/${bucket}/${chemin}`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: { apikey: cle, Authorization: `Bearer ${cle}`, "Content-Type": "application/json" },
|
|
188
|
+
body: "{}",
|
|
189
|
+
});
|
|
190
|
+
if (!r.ok) return null;
|
|
191
|
+
const d = await r.json().catch(() => null);
|
|
192
|
+
const url = d && d.url;
|
|
193
|
+
if (!url) return null;
|
|
194
|
+
return {
|
|
195
|
+
token: (String(url).split("token=")[1] || "").split("&")[0],
|
|
196
|
+
publicUrl: `${base}/storage/v1/object/public/${bucket}/${chemin}`,
|
|
197
|
+
};
|
|
198
|
+
} catch { return null; }
|
|
199
|
+
},
|
|
167
200
|
},
|
|
168
201
|
|
|
169
202
|
db: { request: db.request, selectAll: db.selectAll },
|
|
@@ -285,7 +318,8 @@ function createStandaloneContext(env = process.env) {
|
|
|
285
318
|
},
|
|
286
319
|
|
|
287
320
|
config: {
|
|
288
|
-
|
|
321
|
+
// Sans barre finale : le cœur consomme cette valeur telle quelle. Un seul endroit décide.
|
|
322
|
+
supabaseUrl: sansBarreFinale(env.SUPABASE_URL),
|
|
289
323
|
supabasePublishableKey: env.SUPABASE_PUBLISHABLE_KEY || "",
|
|
290
324
|
mapsKey: env.GOOGLE_MAPS_API_KEY || "",
|
|
291
325
|
extraFrameAncestors: String(env.DOC_FRAME_ANCESTORS || "").split(/\s+/).filter(Boolean),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
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",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"@eslint/js": "^10.0.1",
|
|
70
70
|
"esbuild": "^0.28.2",
|
|
71
71
|
"eslint": "^10.8.1",
|
|
72
|
-
"jsdom": "^
|
|
72
|
+
"jsdom": "^30.0.1",
|
|
73
73
|
"typescript": "^5.5.4",
|
|
74
74
|
"typescript-eslint": "^8.46.4",
|
|
75
75
|
"vitest": "^4.1.10"
|
package/server/handler.js
CHANGED
|
@@ -1172,7 +1172,7 @@ const LEGAL_CSS = `
|
|
|
1172
1172
|
function sendHtml(res, status, html, scriptSrc, imgExtra, frameAncestors) {
|
|
1173
1173
|
res.statusCode = status;
|
|
1174
1174
|
// Origine Supabase Storage (voix ElevenLabs mise en cache dans le bucket public tts-cache) → autorisée en media-src.
|
|
1175
|
-
let supaOrigin; try { supaOrigin = new URL(
|
|
1175
|
+
let supaOrigin; try { supaOrigin = new URL((PLAYER.config && PLAYER.config.supabaseUrl) || "").origin; } catch { supaOrigin = ""; }
|
|
1176
1176
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1177
1177
|
res.setHeader("Cache-Control", "no-store, max-age=0");
|
|
1178
1178
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
@@ -1217,9 +1217,16 @@ function sendSoftWallHtml(res, html, nonce, imgExtra, frameAncestors) {
|
|
|
1217
1217
|
* doit pouvoir être encadrée exactement comme la visionneuse qu'elle remplace — sinon le
|
|
1218
1218
|
* navigateur bloque le rendu et le message de refus ci-dessous n'est jamais émis.
|
|
1219
1219
|
*/
|
|
1220
|
+
// ⚠️ LA MÊME SOURCE QUE LA CARTE D'IDENTITÉ, ET C'EST TOUT LE SUJET. Cette fonction lisait
|
|
1221
|
+
// `process.env.DOC_FRAME_ANCESTORS` pendant que `?contract=1` annonçait
|
|
1222
|
+
// `PLAYER.config.extraFrameAncestors`. Les deux coïncident tant qu'un hôte remplit sa
|
|
1223
|
+
// configuration depuis cette variable — c'est le cas des deux hôtes actuels, donc rien n'était
|
|
1224
|
+
// cassé. Mais un hôte qui la calcule autrement (base, fichier, autre nom de variable) verrait la
|
|
1225
|
+
// carte annoncer une liste et l'en-tête CSP en servir une autre : « configuré » et « servi »
|
|
1226
|
+
// divergents à l'intérieur même du mécanisme construit pour détecter cette divergence.
|
|
1220
1227
|
function embedFrameAncestors() {
|
|
1221
1228
|
return ["'self'", "https://*.vercel.app"]
|
|
1222
|
-
.concat(
|
|
1229
|
+
.concat((PLAYER.config && PLAYER.config.extraFrameAncestors) || [])
|
|
1223
1230
|
.join(" ");
|
|
1224
1231
|
}
|
|
1225
1232
|
|
|
@@ -1580,7 +1587,7 @@ ${LEGAL_CSS}
|
|
|
1580
1587
|
<script nonce="${nonce}">${PLAYER_BROWSER_JS}</script>
|
|
1581
1588
|
<script nonce="${nonce}" src="${PDFJS}/pdf.min.js"></script>
|
|
1582
1589
|
${preview ? `<script nonce="${nonce}" src="${SUPAJS}"></script>
|
|
1583
|
-
<script nonce="${nonce}">var LIVECFG={supaUrl:${JSON.stringify(share.supa_url || "")},supaKey:${JSON.stringify(share.supa_key || "")}};var GMAPS_KEY=${JSON.stringify(
|
|
1590
|
+
<script nonce="${nonce}">var LIVECFG={supaUrl:${JSON.stringify(share.supa_url || "")},supaKey:${JSON.stringify(share.supa_key || "")}};var GMAPS_KEY=${JSON.stringify((PLAYER.config && PLAYER.config.mapsKey) || "" || "")};${LIVE_JS}
|
|
1584
1591
|
${MAP_JS}</script>` : ""}
|
|
1585
1592
|
<script nonce="${nonce}">
|
|
1586
1593
|
(function(){
|
|
@@ -2087,7 +2094,7 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
|
|
|
2087
2094
|
})();
|
|
2088
2095
|
</script>
|
|
2089
2096
|
<script nonce="${nonce}">
|
|
2090
|
-
var LIVECFG={supaUrl:${JSON.stringify(supaUrl || "")},supaKey:${JSON.stringify(supaKey || "")}};var GMAPS_KEY=${JSON.stringify(
|
|
2097
|
+
var LIVECFG={supaUrl:${JSON.stringify(supaUrl || "")},supaKey:${JSON.stringify(supaKey || "")}};var GMAPS_KEY=${JSON.stringify((PLAYER.config && PLAYER.config.mapsKey) || "" || "")};
|
|
2091
2098
|
${LIVE_JS}
|
|
2092
2099
|
${MAP_JS}
|
|
2093
2100
|
(function(){
|
|
@@ -2252,7 +2259,7 @@ async function handler(req, res) {
|
|
|
2252
2259
|
// `spoken` est renvoyé quand il diffère → le viewer aligne le karaoké dessus (mapping mot à mot).
|
|
2253
2260
|
const spoken = (() => { if (!pron) return text; try { const s = pron(text).replace(/\s+/g, " ").trim().slice(0, 700); return s || text; } catch { return text; } })();
|
|
2254
2261
|
const modelId = process.env.ELEVENLABS_MODEL || "eleven_multilingual_v2";
|
|
2255
|
-
const base =
|
|
2262
|
+
const base = (PLAYER.config && PLAYER.config.supabaseUrl) || "";
|
|
2256
2263
|
// « v2 » = version du format de cache : les extraits v1 (sans alignement timestamps) sont ignorés
|
|
2257
2264
|
// d'office et tout se régénère AVEC l'horodatage par caractère (karaoké exact). Anciens fichiers = poids mort minime.
|
|
2258
2265
|
const keyFor = (vid) => crypto.createHash("sha256").update(vid + "|" + modelId + "|v2|" + spoken).digest("hex");
|
|
@@ -2651,8 +2658,8 @@ async function handler(req, res) {
|
|
|
2651
2658
|
await relayerFichier(res, r, null);
|
|
2652
2659
|
return;
|
|
2653
2660
|
}
|
|
2654
|
-
const supaUrl =
|
|
2655
|
-
const supaKey =
|
|
2661
|
+
const supaUrl = (PLAYER.config && PLAYER.config.supabaseUrl) || "";
|
|
2662
|
+
const supaKey = (PLAYER.config && PLAYER.config.supabasePublishableKey) || "";
|
|
2656
2663
|
let alogo = ""; try { alogo = await PLAYER.branding.logo(); } catch { /* sans logo */ }
|
|
2657
2664
|
const anonce = crypto.randomBytes(16).toString("base64");
|
|
2658
2665
|
// Même sujet, trouvé en vérifiant le précédent : cette page ne passait AUCUN paramètre,
|
|
@@ -2674,8 +2681,8 @@ async function handler(req, res) {
|
|
|
2674
2681
|
await relayerFichier(res, r, dispositionInline(q.name));
|
|
2675
2682
|
return;
|
|
2676
2683
|
}
|
|
2677
|
-
const supaUrl =
|
|
2678
|
-
const supaKey =
|
|
2684
|
+
const supaUrl = (PLAYER.config && PLAYER.config.supabaseUrl) || "";
|
|
2685
|
+
const supaKey = (PLAYER.config && PLAYER.config.supabasePublishableKey) || "";
|
|
2679
2686
|
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 || ""))}` };
|
|
2680
2687
|
let plogo = ""; try { plogo = await PLAYER.branding.logo(); } catch { /* sans logo */ }
|
|
2681
2688
|
const pnonce = crypto.randomBytes(16).toString("base64");
|
package/server/presentations.js
CHANGED
|
@@ -130,7 +130,7 @@ async function endPresentation(slug, control) {
|
|
|
130
130
|
// validés par le bucket (image/*+pdf, ≤10 Mo). L'URL publique finale est renvoyée pour attacher au message.
|
|
131
131
|
const ATT_KINDS = { "image/png": "image", "image/jpeg": "image", "image/webp": "image", "image/gif": "image", "application/pdf": "pdf" };
|
|
132
132
|
async function createUploadUrl(slug, name, type) {
|
|
133
|
-
|
|
133
|
+
|
|
134
134
|
// ⚠️ `Object.hasOwn` et pas une simple lecture : un objet littéral hérite de `constructor`,
|
|
135
135
|
// `toString`, `valueOf`, `__proto__`… `ATT_KINDS["constructor"]` rend une FONCTION, donc une
|
|
136
136
|
// valeur vraie — la garde juste en dessous laissait alors passer un type qui n'a jamais été
|
|
@@ -138,14 +138,21 @@ async function createUploadUrl(slug, name, type) {
|
|
|
138
138
|
// action publique. Signalé par un hôte tiers qui venait de trouver la même forme chez lui.
|
|
139
139
|
const demande = String(type || "").toLowerCase();
|
|
140
140
|
const kind = Object.hasOwn(ATT_KINDS, demande) ? ATT_KINDS[demande] : null;
|
|
141
|
-
if (!kind || !slug
|
|
141
|
+
if (!kind || !slug) return { ok: false, status: 400 };
|
|
142
142
|
const safe = (String(name || "fichier").replace(/[^a-zA-Z0-9._-]/g, "_").slice(-60)) || "fichier";
|
|
143
143
|
const path = `${String(slug).replace(/[^a-zA-Z0-9._-]/g, "")}/${Date.now()}-${crypto.randomBytes(4).toString("hex")}-${safe}`;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
|
|
145
|
+
// ⚠️ La signature appartient à l'HÔTE : c'est lui qui détient la clé, pas le cœur. Un hôte qui
|
|
146
|
+
// ne fournit pas cette capacité voit la pièce jointe refusée — et prévenue. Le refus silencieux
|
|
147
|
+
// serait pire : une pièce jointe qui ne part jamais, sans que personne sache que la capacité
|
|
148
|
+
// manque.
|
|
149
|
+
if (!PLAYER.storage || typeof PLAYER.storage.signUpload !== "function") {
|
|
150
|
+
try { PLAYER.errors.capture(new Error("storage.signUpload absent du contexte : les pièces jointes de chat sont indisponibles"), {}); } catch { /* jamais bloquant */ }
|
|
151
|
+
return { ok: false, status: 501 };
|
|
152
|
+
}
|
|
153
|
+
const signe = await PLAYER.storage.signUpload("present-attachments", path);
|
|
154
|
+
if (!signe || !signe.token) return { ok: false, status: 502 };
|
|
155
|
+
return { ok: true, path, token: signe.token, kind, publicUrl: signe.publicUrl };
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
/**
|
|
@@ -177,7 +184,10 @@ function premierPublic(reponse) {
|
|
|
177
184
|
|
|
178
185
|
async function addMessage(slug, { name, email, avatar, isPresenter, isMember, body, replyTo, replyName, replyText, authorToken, attachment }) {
|
|
179
186
|
const b = String(body || "").trim().slice(0, 2000);
|
|
180
|
-
|
|
187
|
+
// ⚠️ Pas de normalisation ici : `config.supabaseUrl` arrive SANS barre finale, c'est le
|
|
188
|
+
// contrat de l'adaptateur. En rajouter une deuxième couche, c'est deux endroits qui décident —
|
|
189
|
+
// et c'est comme ça que `.replace(/\/+$/, "")` s'est retrouvé à cinq exemplaires dans ce dépôt.
|
|
190
|
+
const base = String((PLAYER.config && PLAYER.config.supabaseUrl) || "");
|
|
181
191
|
let att = null;
|
|
182
192
|
if (attachment && typeof attachment === "object" && attachment.url && String(attachment.url).startsWith(base + "/storage/v1/object/public/present-attachments/")) {
|
|
183
193
|
att = { url: String(attachment.url).slice(0, 600), name: String(attachment.name || "").slice(0, 120), type: String(attachment.type || "").slice(0, 60), kind: attachment.kind === "pdf" ? "pdf" : "image" };
|