discovery-media-player 0.1.32 → 0.1.34

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.
@@ -318,6 +318,19 @@ function createStandaloneContext(env = process.env) {
318
318
  *
319
319
  * Non configuré ⇒ l'en-tête est ignoré. Un hôte sans proxy est protégé sans rien faire.
320
320
  */
321
+ /**
322
+ * Comment LIRE votre utilisateur — nom, e-mail, avatar — à partir de ce que `verifyToken`
323
+ * a rendu. Le cœur s'en sert pour signer les messages et les lignes d'assistance : ce qui est
324
+ * prouvé remplace ce que l'appelant affirme.
325
+ *
326
+ * Défaut : les formes usuelles d'un utilisateur Supabase. Redéfinissez-le si le vôtre range
327
+ * son nom ailleurs — sans quoi le message d'un membre porterait le nom qu'il a tapé.
328
+ */
329
+ profileOf(u) {
330
+ const meta = (u && u.user_metadata) || {};
331
+ return { email: (u && u.email) || "", name: meta.name || meta.full_name || "", avatar: meta.avatarUrl || meta.avatar_url || "" };
332
+ },
333
+
321
334
  clientIp(req) {
322
335
  const sauts = Math.max(0, Math.min(10, Number(env.PLAYER_TRUSTED_PROXY_HOPS || 0) || 0));
323
336
  const socket = String((req && req.socket && req.socket.remoteAddress) || "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
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
@@ -1344,11 +1344,38 @@ function cleSessionHote() {
1344
1344
  const CLE_SESSION_PLAYER = "dmp-live-auth";
1345
1345
  const CLE_INVITE = "dmp-present-me";
1346
1346
 
1347
- async function membreVerifie(req) {
1347
+ /**
1348
+ * Le profil PROUVÉ de l'appelant : ce que son jeton dit de lui, jamais ce qu'il affirme.
1349
+ *
1350
+ * ⚠️ `isPresenter` et `isMember` étaient vérifiés depuis 0.1.25/0.1.28, mais `name`, `email` et
1351
+ * `avatar` venaient toujours du corps de la requête — même quand un jeton valide accompagnait
1352
+ * l'appel. Un membre authentifié pouvait donc publier un message portant le nom et l'adresse d'un
1353
+ * collègue, AVEC le badge membre : l'attribution visuelle disait quelqu'un d'autre.
1354
+ *
1355
+ * ⚠️ Ça ne donnait aucun droit — modifier et supprimer s'autorisent par `author_hash`, pas par
1356
+ * l'e-mail (cf. `editMessage`). Le dommage est l'attribution, pas la prise de contrôle. C'est déjà
1357
+ * assez : dans une discussion, un message signé du nom d'un autre est le problème.
1358
+ *
1359
+ * L'hôte peut fournir `identity.profileOf` pour dire comment lire SON utilisateur. Sans ce
1360
+ * crochet, on lit les formes courantes — et l'e-mail, lui, est universel.
1361
+ *
1362
+ * Signalé par la seconde passe d'audit (P1-6).
1363
+ */
1364
+ async function profilDuJeton(req) {
1348
1365
  try {
1349
1366
  const u = await PLAYER.identity.verifyToken((req.headers && req.headers.authorization) || "");
1350
- return !!(u && u.email);
1351
- } catch { return false; }
1367
+ if (!u || !u.email) return null;
1368
+ if (typeof PLAYER.identity.profileOf === "function") {
1369
+ const p = PLAYER.identity.profileOf(u) || {};
1370
+ return { email: String(p.email || u.email), name: String(p.name || ""), avatar: String(p.avatar || "") };
1371
+ }
1372
+ const meta = (u && u.user_metadata) || {};
1373
+ return {
1374
+ email: String(u.email),
1375
+ name: String(u.name || meta.name || meta.full_name || ""),
1376
+ avatar: String(u.avatar || meta.avatarUrl || meta.avatar_url || ""),
1377
+ };
1378
+ } catch { return null; }
1352
1379
  }
1353
1380
 
1354
1381
  async function relayerFichier(res, r, disposition) {
@@ -2738,8 +2765,16 @@ async function handler(req, res) {
2738
2765
  const pres = await getPresentation(String(body.slug || ""));
2739
2766
  if (!pres) return jp(404, { ok: false });
2740
2767
  const estPresentateur = !!(body.control && require("crypto").createHash("sha256").update(String(body.control)).digest("hex") === pres.control_hash);
2741
- const estMembre = await membreVerifie(req);
2742
- const r = await recordAttendance(String(body.slug || ""), { key: body.key, name: body.name, email: body.email, avatar: body.avatar, isMember: estMembre, isPresenter: estPresentateur });
2768
+ // ⚠️ Une identité prouvée REMPLACE celle qu'on affirme — elle ne s'y ajoute pas. Sinon la
2769
+ // vérification ne servirait qu'à décorer une affirmation qu'on croit toujours.
2770
+ const profil = await profilDuJeton(req);
2771
+ const r = await recordAttendance(String(body.slug || ""), {
2772
+ key: body.key,
2773
+ name: (profil && profil.name) || body.name,
2774
+ email: profil ? profil.email : body.email,
2775
+ avatar: (profil && profil.avatar) || body.avatar,
2776
+ isMember: !!profil, isPresenter: estPresentateur,
2777
+ });
2743
2778
  return jp(r.ok ? 200 : (r.status || 400), r);
2744
2779
  } catch { return jp(500, { ok: false }); }
2745
2780
  }
@@ -2782,7 +2817,12 @@ async function handler(req, res) {
2782
2817
  // `isMember` restait l'affirmation du client, alors que `isPresenter` était vérifié juste
2783
2818
  // au-dessus. Deux poids sur la même ligne : le badge « présentateur » se méritait, celui
2784
2819
  // de collègue se réclamait.
2785
- const r = await addMessage(String(body.slug || ""), { name: body.name, email: body.email, avatar: body.avatar, isPresenter: validControl, isMember: await membreVerifie(req), body: body.body, replyTo: body.replyTo, replyName: body.replyName, replyText: body.replyText, authorToken: body.authorToken, attachment: body.attachment });
2820
+ const profil = await profilDuJeton(req);
2821
+ const r = await addMessage(String(body.slug || ""), {
2822
+ name: (profil && profil.name) || body.name,
2823
+ email: profil ? profil.email : body.email,
2824
+ avatar: (profil && profil.avatar) || body.avatar,
2825
+ isPresenter: validControl, isMember: !!profil, body: body.body, replyTo: body.replyTo, replyName: body.replyName, replyText: body.replyText, authorToken: body.authorToken, attachment: body.attachment });
2786
2826
  return jp(r.ok ? 200 : (r.status || 400), r);
2787
2827
  } catch { return jp(500, { ok: false }); }
2788
2828
  }
@@ -2996,15 +3036,30 @@ async function handler(req, res) {
2996
3036
  // légitime dont le bouton pointe vers SON domaine. L'email part de l'hôte, avec sa
2997
3037
  // marque et sa réputation, vers le destinataire choisi par l'attaquant.
2998
3038
  //
2999
- // `PLAYER_PUBLIC_URL` d'abord une valeur que l'exploitant a écrite. Le repli sur
3000
- // `Host` reste pour ne casser aucune instance existante, mais il est SIGNALÉ : une
3001
- // instance qui envoie des emails sans URL publique configurée doit le savoir avant de
3002
- // le découvrir dans un rapport d'hameçonnage.
3039
+ // ⚠️ UNE ALERTE N'EST PAS UNE INTERDICTION.
3040
+ //
3041
+ // 0.1.21 posait `PLAYER_PUBLIC_URL`, retombait sur `Host` quand elle manquait, et
3042
+ // SIGNALAIT le repli. C'était le bon réflexe de compatibilité et la mauvaise
3043
+ // conclusion : le journal ne bloque pas un email d'hameçonnage. Une instance mal
3044
+ // configurée continuait d'envoyer, signée de sa marque, avec un bouton pointant où le
3045
+ // lecteur voulait — et l'exploitant l'apprenait dans un rapport d'abus.
3046
+ //
3047
+ // ⚠️ CE QU'ON REFUSE, C'EST L'ENVOI — PAS LE LIEN. Le lien enfant est créé, tracé, et
3048
+ // rendu à l'appelant : il peut le transmettre lui-même. Ce qui est retenu est la seule
3049
+ // chose qu'on ne peut pas rattraper — un courrier parti de nos serveurs, avec notre
3050
+ // domaine dans l'en-tête et notre réputation d'expéditeur derrière.
3051
+ //
3052
+ // La compatibilité invoquée en 0.1.21 ne tenait donc pas : refuser l'envoi ne casse
3053
+ // pas la création du lien, qui est la fonction principale de cette route.
3054
+ //
3055
+ // Signalé par la seconde passe d'audit (P1-1).
3003
3056
  const publique = String(PLAYER.legal.publicUrl || "").trim();
3004
3057
  if (!publique) {
3005
- try { PLAYER.errors.capture(new Error("PLAYER_PUBLIC_URL non configurée : le lien de l'email est construit depuis l'en-tête Host, que le client choisit"), { route: "reshare" }); } catch { /* jamais bloquant */ }
3058
+ try { PLAYER.errors.capture(new Error("PLAYER_PUBLIC_URL non configurée : envoi refusé (le lien de l'email serait construit depuis l'en-tête Host, que le client choisit)"), { route: "reshare" }); } catch { /* jamais bloquant */ }
3059
+ refusEnvoi = "public-url-unconfigured";
3060
+ throw new Error("URL publique non configurée");
3006
3061
  }
3007
- const origin = publique || `https://${req.headers.host}`;
3062
+ const origin = publique;
3008
3063
  const r = await sendReshareEmail({ parent, childSlug: out.slug, origin, toEmail: mail, toName: body.name });
3009
3064
  sent = !!(r && r.sent);
3010
3065
  } catch { /* best-effort : le lien existe quand même */ }