discovery-media-player 0.1.46 → 0.1.48
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
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",
|
package/server/handler.js
CHANGED
|
@@ -29,6 +29,9 @@ function init(ctx) {
|
|
|
29
29
|
require("./shares").init(ctx);
|
|
30
30
|
require("./presentations").init(ctx);
|
|
31
31
|
require("./brands").init(ctx);
|
|
32
|
+
// ⚠️ Réinitialisé avec le contexte : les réponses de la sonde valent pour UNE base. Un hôte qui
|
|
33
|
+
// rebranche son contexte sur un autre projet doit reposer la question, pas hériter des réponses.
|
|
34
|
+
require("./schema").init(ctx);
|
|
32
35
|
docbot = ctx.plugins.bot;
|
|
33
36
|
brandIntroRuntime = ctx.plugins.brandIntro && ctx.plugins.brandIntro.brandIntroRuntime;
|
|
34
37
|
botBrowser = ctx.plugins.botBrowser;
|
|
@@ -2409,6 +2412,53 @@ ${botOn && botBrowser ? botBrowser.botViewerJs(ICONS) : ""}
|
|
|
2409
2412
|
|
|
2410
2413
|
// CSP de la page audience (Présenter) : autorise pdf.js (cdnjs), supabase-js (jsdelivr) et la connexion
|
|
2411
2414
|
// Realtime (https + wss vers le projet Supabase). Plus permissive que la visionneuse, limitée à cette page.
|
|
2415
|
+
/**
|
|
2416
|
+
* Les origines d'images qu'une page a le droit de charger.
|
|
2417
|
+
*
|
|
2418
|
+
* ⚠️ FOURNIR UNE URL SANS AUTORISER SON ORIGINE REVIENT À NE PAS LA FOURNIR — avec l'apparence du
|
|
2419
|
+
* contraire. Le HTML est parfait, le fichier répond 200, et le navigateur refuse quand même. C'est
|
|
2420
|
+
* arrivé en 0.1.47 : la marque du client était résolue, écrite dans la page, et bloquée. Le chemin
|
|
2421
|
+
* du lien tracé ajoutait bien son origine ; celui de l'aperçu, non. Deux politiques sur la même
|
|
2422
|
+
* instance, à la même minute.
|
|
2423
|
+
*
|
|
2424
|
+
* ⚠️ ET AUCUNE SONDE SERVEUR NE PEUT LE VOIR. Le HTML rendu est correct, le script se compile, le
|
|
2425
|
+
* paquet est conforme. Ni notre étape de fumée, ni la garde d'artefact, ni un test qui exécute la
|
|
2426
|
+
* page ne mordent : seul un navigateur le montre. C'est le second hôte qui l'a vu, à l'œil, chez son
|
|
2427
|
+
* client.
|
|
2428
|
+
*
|
|
2429
|
+
* D'où cette fonction : une seule liste, pour toutes les routes qui rendent la visionneuse. La
|
|
2430
|
+
* remplir est une décision ; l'oublier n'est plus possible, parce qu'il n'y a plus qu'un endroit.
|
|
2431
|
+
*
|
|
2432
|
+
* ⚠️ CE QU'ELLE NE COUVRE PAS, ET QUI EST UN AUTRE PROBLÈME. La page d'AUDIENCE affiche les avatars
|
|
2433
|
+
* des participants, qui arrivent par la présence — donc à l'exécution, et depuis autant d'origines
|
|
2434
|
+
* qu'il y a de membres chez l'hôte. Aucune liste posée au rendu ne peut les prévoir. Les
|
|
2435
|
+
* pré-autoriser demanderait d'élargir la politique à une origine d'hôte entière, ce qui est une
|
|
2436
|
+
* décision à prendre séparément, pas un oubli à corriger ici.
|
|
2437
|
+
*
|
|
2438
|
+
* ⚠️ ON NE DÉRIVE PAS CETTE LISTE DU HTML RENDU, et c'est délibéré. Ce serait plus général et ça
|
|
2439
|
+
* viderait la politique de son sens : autoriser tout ce que la page référence, c'est autoriser aussi
|
|
2440
|
+
* ce qu'une valeur mal filtrée y aurait glissé. La liste porte des CHAMPS connus, pas des URL
|
|
2441
|
+
* trouvées.
|
|
2442
|
+
*/
|
|
2443
|
+
function originesImages(logoInstance, share) {
|
|
2444
|
+
const s = share || {};
|
|
2445
|
+
return [
|
|
2446
|
+
originOf(logoInstance),
|
|
2447
|
+
originOf(s.brand_logo),
|
|
2448
|
+
originOf(s.bot_avatar),
|
|
2449
|
+
// ⚠️ Celui-ci ne cassait pas, et c'est pire qu'un défaut visible : il marchait PAR ACCIDENT,
|
|
2450
|
+
// parce que la photo du présentateur et l'avatar de l'assistant sortent en général du même
|
|
2451
|
+
// stockage, donc de la même origine. Le jour où un hôte range l'une ailleurs, elle disparaît
|
|
2452
|
+
// sans que rien n'ait changé chez lui.
|
|
2453
|
+
originOf(s.bot_vphoto),
|
|
2454
|
+
// ⚠️ Trouvé par la garde à son premier passage, et il ne se voyait pas : cette adresse ne part
|
|
2455
|
+
// pas dans le HTML mais dans la configuration, et c'est la couche live qui en fait une image à
|
|
2456
|
+
// l'exécution — dans la liste des participants. Un défaut de politique sur une image construite
|
|
2457
|
+
// par du script se lit encore moins qu'un autre : la page est déjà chargée quand il se produit.
|
|
2458
|
+
originOf(s.presenter_avatar),
|
|
2459
|
+
].filter(Boolean).join(" ");
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2412
2462
|
function sendPresentHtml(res, html, nonce, supaUrl, imgExtra, frameAncestors) {
|
|
2413
2463
|
const wss = String(supaUrl || "").replace(/^https:/, "wss:");
|
|
2414
2464
|
res.statusCode = 200;
|
|
@@ -3614,7 +3664,18 @@ async function handler(req, res) {
|
|
|
3614
3664
|
}
|
|
3615
3665
|
const supaUrl = (PLAYER.config && PLAYER.config.supabaseUrl) || "";
|
|
3616
3666
|
const supaKey = (PLAYER.config && PLAYER.config.supabasePublishableKey) || "";
|
|
3617
|
-
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 || ""), internal_token: String(q.it || ""), 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 || ""))}` };
|
|
3667
|
+
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 || ""), internal_token: String(q.it || ""), supa_url: supaUrl, supa_key: supaKey, auto_present: String(q.autopresent || "") === "1", resume_slug: String(q.resume || ""), brand_key: String(q.brand || "") || null, stream_url: `/api/doc?preview=1&stream=1&url=${encodeURIComponent(url)}&name=${encodeURIComponent(String(q.name || ""))}` };
|
|
3668
|
+
// ⚠️ LA MARQUE MANQUAIT ICI, ET SEULEMENT ICI. Toute la machinerie existe — l'hôte répond à
|
|
3669
|
+
// `PLAYER_HOST_BRAND_URL`, `branding.forKey` résout, les liens tracés affichent la bonne
|
|
3670
|
+
// marque. Ce chemin-ci ne l'appelait simplement pas, et aucun paramètre ne transportait la
|
|
3671
|
+
// clé. Un hôte à plusieurs marques servait donc, sur le domaine d'un client, le loader d'un
|
|
3672
|
+
// autre — le visiteur voyant l'enseigne d'une entreprise qu'il ne connaît pas.
|
|
3673
|
+
//
|
|
3674
|
+
// Signalé par le second hôte, sur une notice ouverte chez son client.
|
|
3675
|
+
try {
|
|
3676
|
+
const marque = await brands.brandForShare(pseudo);
|
|
3677
|
+
if (marque) { pseudo.brand_logo = marque.logo; pseudo.brand_name = marque.name; pseudo.brand_dark = marque.dark; }
|
|
3678
|
+
} catch { /* le loader dégrade, il n'empêche pas de lire */ }
|
|
3618
3679
|
let plogo = ""; try { plogo = await PLAYER.branding.logo(); } catch { /* sans logo */ }
|
|
3619
3680
|
const pnonce = crypto.randomBytes(16).toString("base64");
|
|
3620
3681
|
// Aperçu interne : CSP relâchée (supabase-js jsdelivr + Realtime wss) pour la présence + le chat live,
|
|
@@ -3629,7 +3690,7 @@ async function handler(req, res) {
|
|
|
3629
3690
|
// Conséquence absurde relevée par cet hôte : la page de REFUS, corrigée la veille, était
|
|
3630
3691
|
// encadrable chez lui — pas la page de SUCCÈS. Le chemin d'erreur était plus portable que
|
|
3631
3692
|
// le chemin nominal.
|
|
3632
|
-
return sendPresentHtml(res, viewerHtml(pseudo, pnonce, plogo), pnonce, supaUrl,
|
|
3693
|
+
return sendPresentHtml(res, viewerHtml(pseudo, pnonce, plogo), pnonce, supaUrl, originesImages(plogo, pseudo),
|
|
3633
3694
|
embed ? embedFrameAncestors() : "'self'");
|
|
3634
3695
|
}
|
|
3635
3696
|
|
|
@@ -3728,7 +3789,7 @@ async function handler(req, res) {
|
|
|
3728
3789
|
const frameAncestors = share.embed
|
|
3729
3790
|
? embedFrameAncestors()
|
|
3730
3791
|
: "'self'";
|
|
3731
|
-
return sendHtml(res, 200, viewerHtml(share, nonce, logoUrl, pitch), `'nonce-${nonce}'`,
|
|
3792
|
+
return sendHtml(res, 200, viewerHtml(share, nonce, logoUrl, pitch), `'nonce-${nonce}'`, originesImages(logoUrl, share), frameAncestors);
|
|
3732
3793
|
} catch (error) {
|
|
3733
3794
|
try { await PLAYER.errors.capture(error, { route: "doc", method: req.method }); } catch { /* ignore */ }
|
|
3734
3795
|
res.statusCode = 500; res.end("Erreur");
|
package/server/schema.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// CE QUE LA BASE PORTE VRAIMENT, ET NON CE QU'ON CROIT LUI AVOIR APPLIQUÉ.
|
|
2
|
+
//
|
|
3
|
+
// ⚠️ LE PLAYER N'APPLIQUE PAS LES MIGRATIONS, ET NE LE POURRA JAMAIS. Il parle à la base uniquement
|
|
4
|
+
// par PostgREST, qui n'exécute pas de DDL. Lui donner ce pouvoir supposerait d'exposer une fonction
|
|
5
|
+
// capable d'exécuter du SQL arbitraire — dans un service qui sert des liens publics. C'est l'hôte qui
|
|
6
|
+
// applique ; le player doit seulement SAVOIR.
|
|
7
|
+
//
|
|
8
|
+
// ⚠️ ET IL DEMANDE PLUTÔT QU'IL NE RETIENT. Une table de suivi des migrations aurait dû être créée
|
|
9
|
+
// par une migration : le premier pas serait retombé sur le problème qu'elle résout. Et un registre
|
|
10
|
+
// dit ce qu'on CROIT avoir appliqué ; une sonde dit ce qui EST. Les deux divergent le jour où
|
|
11
|
+
// quelqu'un applique à la main — c'est-à-dire le jour où ça compte.
|
|
12
|
+
//
|
|
13
|
+
// ⚠️ POURQUOI CE FICHIER EXISTE. PostgREST rejette un `PATCH` portant une colonne inconnue. Un hôte
|
|
14
|
+
// qui déploie le code avant la migration voit donc TOUTES ses écritures échouer sur ce chemin, pas
|
|
15
|
+
// seulement la fonction nouvelle — et le message parle d'une colonne, pas d'une version. Deux
|
|
16
|
+
// chantiers ont été repoussés pour cette seule raison. Avec cette sonde, l'ordre de déploiement
|
|
17
|
+
// cesse d'être un piège.
|
|
18
|
+
|
|
19
|
+
let PLAYER = null;
|
|
20
|
+
/**
|
|
21
|
+
* Une question posée une fois, retenue pour le processus.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ C'EST AUSSI CE QUI DÉDOUBLONNE LE JOURNAL, et il n'y a donc rien d'autre à écrire pour ça. La
|
|
24
|
+
* première version portait un second ensemble « déjà signalé » : vidé exactement quand celui-ci
|
|
25
|
+
* l'est, donc inatteignable. Une mutation qui le retirait ne faisait échouer aucun test — la bonne
|
|
26
|
+
* réponse n'était pas d'ajouter un test pour le justifier, c'était de constater qu'il ne servait à
|
|
27
|
+
* rien. Une garde qu'on ne peut pas voir refuser n'est pas une garde.
|
|
28
|
+
*/
|
|
29
|
+
const connues = new Map();
|
|
30
|
+
|
|
31
|
+
function init(ctx) {
|
|
32
|
+
PLAYER = ctx;
|
|
33
|
+
connues.clear();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* La base porte-t-elle cette colonne ?
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ EN CAS DE DOUTE, ABSENTE. Supposer présente ferait échouer l'écriture ENTIÈRE — la nouvelle
|
|
40
|
+
* fonction et tout ce qui l'accompagne. Supposer absente fait attendre la fonction seule. Une
|
|
41
|
+
* fonction qui attend vaut mieux qu'une écriture perdue, et c'est la seule direction où l'erreur se
|
|
42
|
+
* répare toute seule quand la migration arrive.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} table
|
|
45
|
+
* @param {string} colonne
|
|
46
|
+
* @param {string} migration le fichier à appliquer — c'est LUI qu'on nomme dans le journal
|
|
47
|
+
*/
|
|
48
|
+
function aLaColonne(table, colonne, migration) {
|
|
49
|
+
const cle = `${table}.${colonne}`;
|
|
50
|
+
if (!connues.has(cle)) connues.set(cle, sonder(table, colonne, migration, cle));
|
|
51
|
+
return connues.get(cle);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function sonder(table, colonne, migration, cle) {
|
|
55
|
+
try {
|
|
56
|
+
// `limit=0` : on ne veut aucune ligne, seulement savoir si la colonne se sélectionne. PostgREST
|
|
57
|
+
// répond 400 « column … does not exist » quand elle manque — donc l'échec EST la réponse.
|
|
58
|
+
//
|
|
59
|
+
// ⚠️ LA PART ENCODÉE EST CALCULÉE À PART, ET PAS POUR LA LISIBILITÉ. La garde de portabilité de
|
|
60
|
+
// la CI traque la syntaxe propre à PostgREST — les ressources imbriquées « select=a(b) », les
|
|
61
|
+
// arbres booléens — en cherchant une parenthèse après « select= ». Écrite dans le gabarit,
|
|
62
|
+
// l'appel à encodeURIComponent en produisait une : la garde accusait une requête parfaitement
|
|
63
|
+
// portable. On lève l'ambiguïté du côté du code, pas du côté de la garde.
|
|
64
|
+
//
|
|
65
|
+
// ⚠️ ET CETTE SONDE EST LE SEUL ENDROIT QUI DÉPEND DU COMPORTEMENT D'ERREUR DE PostgREST. Sur
|
|
66
|
+
// une autre base, « la colonne manque » se demanderait autrement. C'est isolé ici exprès : un
|
|
67
|
+
// portage a un fichier à réécrire, pas une habitude à retrouver partout.
|
|
68
|
+
const champ = encodeURIComponent(colonne);
|
|
69
|
+
await PLAYER.db.request(`${table}?select=${champ}&limit=0`);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
// ⚠️ ON NE DISTINGUE PAS « COLONNE ABSENTE » DE « BASE INJOIGNABLE », ET C'EST VOULU. Les deux
|
|
73
|
+
// mènent à la même décision — ne pas écrire ce champ — et distinguer supposerait de lire un
|
|
74
|
+
// message d'erreur, c'est-à-dire de dépendre du texte d'un service tiers. Ce qui change entre
|
|
75
|
+
// les deux, c'est la durée : une base injoignable le redevient, et le processus suivant reposera
|
|
76
|
+
// la question.
|
|
77
|
+
signaler(cle, migration);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* ⚠️ ON NOMME LE FICHIER, PAS L'ERREUR. « column does not exist » envoie l'exploitant lire du
|
|
84
|
+
* PostgREST ; « appliquez supabase/migrations/0001-…sql » lui dit quoi faire. La différence entre
|
|
85
|
+
* les deux se compte en heures.
|
|
86
|
+
*/
|
|
87
|
+
function signaler(cle, migration) {
|
|
88
|
+
const quoi = migration ? `Appliquez ${migration}.` : "Une migration est en attente.";
|
|
89
|
+
try {
|
|
90
|
+
console.warn(`[player] la colonne « ${cle} » manque : la fonction qui en dépend reste inactive. ${quoi}`);
|
|
91
|
+
} catch { /* sans console */ }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Pour les tests et l'exploitation : reposer la question. */
|
|
95
|
+
function oublier() { connues.clear(); }
|
|
96
|
+
|
|
97
|
+
module.exports = { init, aLaColonne, oublier };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- 0001 — le destinataire ATTESTÉ par l'hôte, séparé de qui peut expédier en son nom
|
|
2
|
+
--
|
|
3
|
+
-- Pour : un hôte qui identifie lui-même son visiteur (code à usage unique, espace projet) et veut
|
|
4
|
+
-- que ses lectures soient comptées, attribuées et révocables — sans lui donner de lien
|
|
5
|
+
-- anonyme, et sans exiger le jeton d'un membre qu'il n'a pas.
|
|
6
|
+
--
|
|
7
|
+
-- Sans lui : rien ne change. Les liens attestés ne peuvent pas être créés, et le player refuse la
|
|
8
|
+
-- demande en le disant. Aucune fonction existante n'est affectée.
|
|
9
|
+
--
|
|
10
|
+
-- Sûre pendant que la version précédente tourne : oui — additive, et personne ne lit ni n'écrit
|
|
11
|
+
-- cette colonne tant que le code qui la connaît n'est pas déployé.
|
|
12
|
+
--
|
|
13
|
+
-- ⚠️ POURQUOI UNE COLONNE PLUTÔT QU'UN DRAPEAU. `recipient_email` porte aujourd'hui DEUX faits :
|
|
14
|
+
-- « à qui ce lien est destiné » et « qui a le droit d'expédier en son nom au repartage ». Un lien
|
|
15
|
+
-- attesté par l'hôte doit avoir le premier sans le second — son visiteur n'a jamais engagé sa
|
|
16
|
+
-- responsabilité chez nous.
|
|
17
|
+
--
|
|
18
|
+
-- Un drapeau `atteste_par_hote` dirait « ne fais pas la chose » : il décrirait le correctif au lieu
|
|
19
|
+
-- du fait, et quelqu'un l'ignorerait dans six mois pour un cas qui semblerait différent. Avec deux
|
|
20
|
+
-- colonnes, la garde d'envoi (qui exige `recipient_email`) et l'héritage du repartage refusent tous
|
|
21
|
+
-- deux SANS SAVOIR POURQUOI on les protège. La règle devient une conséquence de la donnée.
|
|
22
|
+
--
|
|
23
|
+
-- Formulé par le second hôte, dont la demande a produit la séparation.
|
|
24
|
+
|
|
25
|
+
alter table public.commercial_doc_shares
|
|
26
|
+
add column if not exists attested_recipient_email text;
|
|
27
|
+
|
|
28
|
+
comment on column public.commercial_doc_shares.attested_recipient_email is
|
|
29
|
+
'Destinataire attesté par l''hôte : sert à ATTRIBUER une lecture, jamais à expédier en son nom. '
|
|
30
|
+
'La colonne recipient_email, elle, dit qui peut expédier — vide quand personne ne le peut.';
|