discovery-media-player 0.1.95 → 0.1.96
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 +45 -0
- package/package.json +1 -1
- package/server/handler.js +4 -0
package/context/standalone.js
CHANGED
|
@@ -511,6 +511,48 @@ function createStandaloneContext(env = process.env) {
|
|
|
511
511
|
} catch { return null; }
|
|
512
512
|
},
|
|
513
513
|
|
|
514
|
+
/**
|
|
515
|
+
* JETON DE PRÉSENCE (P1c étape 2) — lie `slug + key + exp`, signé par l'hôte.
|
|
516
|
+
*
|
|
517
|
+
* ⚠️ CE QU'IL PROUVE, ET CE QU'IL NE PROUVE PAS. Il prouve que l'hôte a ÉMIS ce couple
|
|
518
|
+
* (slug, key) : un battement qui le porte prouve donc que son porteur détient la clé que l'hôte
|
|
519
|
+
* lui a donnée — assez pour empêcher un TIERS d'écraser une présence déjà enregistrée. Il ne
|
|
520
|
+
* prouve PAS qu'il y a une vraie personne derrière (un visiteur peut demander plusieurs jetons) :
|
|
521
|
+
* le plafond de création de l'étape 1 reste nécessaire.
|
|
522
|
+
*
|
|
523
|
+
* ⚠️ SECRET PAR INSTANCE, PAS PAR MARQUE. `PLAYER_PRESENCE_SECRET` est partagé par toutes les
|
|
524
|
+
* marques d'une même instance : un jeton émis sur l'une valide sur l'autre. Inoffensif — la vraie
|
|
525
|
+
* portée est le `slug`, qui est DANS la charge signée et vérifié à l'usage. Même comparaison à
|
|
526
|
+
* temps constant et même `exp` obligatoire que le jeton interne.
|
|
527
|
+
*/
|
|
528
|
+
signPresenceToken(slug, key, ttlSeconds) {
|
|
529
|
+
const secret = String(env.PLAYER_PRESENCE_SECRET || "");
|
|
530
|
+
if (!secret || !slug || !key) return "";
|
|
531
|
+
const exp = Math.floor(Date.now() / 1000) + Math.max(60, Math.trunc(Number(ttlSeconds)) || 3600);
|
|
532
|
+
const corps = Buffer.from(JSON.stringify({ slug: String(slug), key: String(key), exp }), "utf8").toString("base64url");
|
|
533
|
+
const sig = crypto.createHmac("sha256", secret).update(corps).digest("base64url");
|
|
534
|
+
return `${corps}.${sig}`;
|
|
535
|
+
},
|
|
536
|
+
verifyPresenceToken(jeton) {
|
|
537
|
+
const secret = String(env.PLAYER_PRESENCE_SECRET || "");
|
|
538
|
+
const brut = String(jeton || "");
|
|
539
|
+
if (!secret || !brut) return null;
|
|
540
|
+
const sep = brut.lastIndexOf(".");
|
|
541
|
+
if (sep <= 0) return null;
|
|
542
|
+
const corps = brut.slice(0, sep);
|
|
543
|
+
const signature = brut.slice(sep + 1);
|
|
544
|
+
try {
|
|
545
|
+
const attendue = crypto.createHmac("sha256", secret).update(corps).digest("base64url");
|
|
546
|
+
const a = Buffer.from(attendue, "utf8"); const b = Buffer.from(signature, "utf8");
|
|
547
|
+
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return null;
|
|
548
|
+
const d = JSON.parse(Buffer.from(corps, "base64url").toString("utf8"));
|
|
549
|
+
if (!d || typeof d !== "object") return null;
|
|
550
|
+
if (!Number.isFinite(d.exp) || d.exp * 1000 < Date.now()) return null;
|
|
551
|
+
if (!d.slug || !d.key) return null;
|
|
552
|
+
return { slug: String(d.slug), key: String(d.key) };
|
|
553
|
+
} catch { return null; }
|
|
554
|
+
},
|
|
555
|
+
|
|
514
556
|
// ⚠️ Le rôle vient d'`app_metadata`, JAMAIS d'`user_metadata` : ce dernier est modifiable par
|
|
515
557
|
// l'utilisateur lui-même. S'y fier revient à laisser chacun choisir ses droits.
|
|
516
558
|
roleOf: (user) => String(((user || {}).app_metadata || {}).role || "").trim().toLowerCase(),
|
|
@@ -631,6 +673,9 @@ function createStandaloneContext(env = process.env) {
|
|
|
631
673
|
hostMail: !!String(env.PLAYER_HOST_MAIL_URL || "").trim(),
|
|
632
674
|
// Porte fermée : sans jeton de l'hôte, aucune session interne n'est écrite.
|
|
633
675
|
internalStrict: String(env.PLAYER_INTERNAL_STRICT || "") === "1",
|
|
676
|
+
// Une fois les bundles déployés des deux côtés ET `presence.sansJeton` retombé à zéro : un
|
|
677
|
+
// battement sans jeton de présence valide cesse d'être enregistré. Off par défaut (transition).
|
|
678
|
+
presenceStrict: String(env.PLAYER_PRESENCE_STRICT || "") === "1",
|
|
634
679
|
},
|
|
635
680
|
};
|
|
636
681
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.96",
|
|
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
|
@@ -595,6 +595,10 @@ async function handler(req, res) {
|
|
|
595
595
|
// MESURABLE : un cockpit peut refuser une instance non stricte, au lieu de le découvrir en
|
|
596
596
|
// lisant un journal. Demandé par le second hôte (cinquième audit, P1-4).
|
|
597
597
|
internalStrict: !!(PLAYER.config && PLAYER.config.internalStrict),
|
|
598
|
+
// ⚠️ La porte du jeton de présence (P1c étape 2) est-elle fermée ? Même raison
|
|
599
|
+
// qu'internalStrict : un cockpit doit voir l'état des DEUX hôtes sans lire un journal — c'est
|
|
600
|
+
// ce booléen, lu avec `presence: { avecJeton, sansJeton }` de la carte, qui dit quand fermer.
|
|
601
|
+
presenceStrict: !!(PLAYER.config && PLAYER.config.presenceStrict),
|
|
598
602
|
// ⚠️ LE BALAYAGE DE RÉTENTION EST-IL ARMÉ ? La capacité `retention` dit que l'instance PEUT
|
|
599
603
|
// purger ; ce booléen dit si le balayage automatique TOURNE (`config.retention.balayage`).
|
|
600
604
|
// Sans lui, une instance armée est indiscernable d'une instance éteinte — et une purge qui
|