discovery-media-player 0.1.47 → 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.47",
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;
@@ -3640,7 +3690,7 @@ async function handler(req, res) {
3640
3690
  // Conséquence absurde relevée par cet hôte : la page de REFUS, corrigée la veille, était
3641
3691
  // encadrable chez lui — pas la page de SUCCÈS. Le chemin d'erreur était plus portable que
3642
3692
  // le chemin nominal.
3643
- return sendPresentHtml(res, viewerHtml(pseudo, pnonce, plogo), pnonce, supaUrl, originOf(plogo),
3693
+ return sendPresentHtml(res, viewerHtml(pseudo, pnonce, plogo), pnonce, supaUrl, originesImages(plogo, pseudo),
3644
3694
  embed ? embedFrameAncestors() : "'self'");
3645
3695
  }
3646
3696
 
@@ -3739,7 +3789,7 @@ async function handler(req, res) {
3739
3789
  const frameAncestors = share.embed
3740
3790
  ? embedFrameAncestors()
3741
3791
  : "'self'";
3742
- return sendHtml(res, 200, viewerHtml(share, nonce, logoUrl, pitch), `'nonce-${nonce}'`, [originOf(logoUrl), originOf(share.bot_avatar), originOf(share.brand_logo)].filter(Boolean).join(" "), frameAncestors);
3792
+ return sendHtml(res, 200, viewerHtml(share, nonce, logoUrl, pitch), `'nonce-${nonce}'`, originesImages(logoUrl, share), frameAncestors);
3743
3793
  } catch (error) {
3744
3794
  try { await PLAYER.errors.capture(error, { route: "doc", method: req.method }); } catch { /* ignore */ }
3745
3795
  res.statusCode = 500; res.end("Erreur");
@@ -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.';