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.
@@ -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.95",
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