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.
- package/context/standalone.js +13 -0
- package/package.json +1 -1
- package/server/handler.js +67 -12
package/context/standalone.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
1351
|
-
|
|
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
|
-
|
|
2742
|
-
|
|
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
|
|
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
|
-
//
|
|
3000
|
-
//
|
|
3001
|
-
//
|
|
3002
|
-
// le
|
|
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
|
|
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
|
|
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 */ }
|