discovery-media-player 0.1.7 → 0.1.8

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.
@@ -15,9 +15,31 @@
15
15
 
16
16
  const storage = require("./storage");
17
17
 
18
+ /**
19
+ * Retire les barres finales d'une base d'URL.
20
+ *
21
+ * Sans expression régulière. L'analyse statique a signalé `.replace(/\/+$/, "")` dès son premier
22
+ * passage, sur les deux lignes que je venais d'écrire.
23
+ *
24
+ * Mesuré avant de corriger : V8 traite ce motif en temps linéaire (200 000 barres, moins d'une
25
+ * milliseconde), et l'entrée vient de toute façon d'une variable d'environnement — donc de
26
+ * l'exploitant, pas d'un visiteur. Ce n'était donc PAS une lenteur réelle ici.
27
+ *
28
+ * ⚠️ On change quand même, pour une raison qui n'est pas celle de l'alerte : cette forme se
29
+ * recopie. Elle est déjà à cinq endroits du dépôt, et la prochaine copie tombera peut-être sur
30
+ * une entrée venue du dehors, dans un moteur moins clément. Une boucle qui ne peut pas revenir en
31
+ * arrière retire la classe, pas l'occurrence — et évite d'apprendre à ignorer l'alerte.
32
+ */
33
+ function sansBarreFinale(valeur) {
34
+ const s = String(valeur || "");
35
+ let fin = s.length;
36
+ while (fin > 0 && s.charCodeAt(fin - 1) === 47) fin--;
37
+ return s.slice(0, fin);
38
+ }
39
+
18
40
  /** Client REST minimal (PostgREST). Absent de configuration ⇒ chaque appel échoue franchement. */
19
41
  function creerDb(env) {
20
- const url = String(env.SUPABASE_URL || "").replace(/\/+$/, "");
42
+ const url = sansBarreFinale(env.SUPABASE_URL);
21
43
  const cle = String(env.SUPABASE_SERVICE_ROLE_KEY || "");
22
44
 
23
45
  async function request(chemin, options = {}) {
@@ -151,11 +173,44 @@ function createStandaloneContext(env = process.env) {
151
173
  mail: { async send() { return null; } },
152
174
 
153
175
  identity: {
154
- /** Vérifie un jeton auprès de Supabase Auth. Sans base : personne n'est authentifié. */
176
+ /**
177
+ * Vérifie un jeton auprès de Supabase Auth. Sans émetteur : personne n'est authentifié.
178
+ *
179
+ * ⚠️ LA BASE DU PLAYER ET L'ÉMETTEUR DES JETONS SONT DEUX CHOSES DIFFÉRENTES.
180
+ *
181
+ * `SUPABASE_URL` servait les deux rôles. Vrai tant que le player et son application
182
+ * partagent un déploiement — et faux par construction dès qu'une instance est séparée : la
183
+ * base appartient au player, l'identité appartient à l'hôte. Les membres de l'hôte
184
+ * recevaient donc des jetons émis par un projet, vérifiés contre un autre : toute la moitié
185
+ * « membre » de la surface (diffusion, statistiques, présentations authentifiées) était
186
+ * hors d'atteinte. Signalé par le second hôte, c'est la troisième hypothèse de cette forme
187
+ * en deux jours — elles ne se voient qu'en exerçant la séparation.
188
+ *
189
+ * `PLAYER_AUTH_URL` désigne donc l'émetteur, et `SUPABASE_URL` reste la base. Absente, on
190
+ * retombe sur `SUPABASE_URL` : une instance où les deux coïncident ne change pas d'un
191
+ * caractère.
192
+ *
193
+ * ⚠️ ET LA CLÉ NE RETOMBE PAS, ELLE. Le repli historique allait jusqu'à
194
+ * `SUPABASE_SERVICE_ROLE_KEY` — la clé maîtresse de NOTRE base. Tant que l'émetteur était
195
+ * notre propre projet, c'était sans conséquence ; vers un émetteur tiers, ce serait
196
+ * l'envoyer à un serveur qui n'a rien à en faire. Un émetteur distinct exige donc sa propre
197
+ * clé publiable, et son absence se dit au lieu de se replier.
198
+ */
155
199
  async verifyToken(authorization) {
156
200
  const jeton = String(authorization || "").replace(/^Bearer\s+/i, "").trim();
157
- const url = String(env.SUPABASE_URL || "").replace(/\/+$/, "");
158
- const cle = String(env.SUPABASE_PUBLISHABLE_KEY || env.SUPABASE_SERVICE_ROLE_KEY || "");
201
+ const emetteur = sansBarreFinale(env.PLAYER_AUTH_URL);
202
+ const base = sansBarreFinale(env.SUPABASE_URL);
203
+
204
+ const url = emetteur || base;
205
+ const cle = emetteur
206
+ ? String(env.PLAYER_AUTH_KEY || "")
207
+ : String(env.SUPABASE_PUBLISHABLE_KEY || env.SUPABASE_SERVICE_ROLE_KEY || "");
208
+
209
+ if (emetteur && !cle) {
210
+ // Le refus silencieux est le piège de cette configuration : sans clé, chaque membre est
211
+ // simplement « non authentifié », ce qui ressemble à un droit manquant. On le dit.
212
+ try { journal.capture(new Error("PLAYER_AUTH_URL est configurée sans PLAYER_AUTH_KEY : aucun jeton ne peut être vérifié"), {}); } catch { /* ignore */ }
213
+ }
159
214
  if (!jeton || !url || !cle) return null;
160
215
  try {
161
216
  const r = await fetch(`${url}/auth/v1/user`, {
@@ -25,8 +25,8 @@ need.
25
25
  {
26
26
  "product": "discovery-media-player",
27
27
  "contract": 1,
28
- "version": "0.1.7",
29
- "capabilities": ["docshare", "presentations", "embed-denied", "host-fetch", "brand-reference"],
28
+ "version": "0.1.8",
29
+ "capabilities": ["docshare", "presentations", "embed-denied", "host-fetch", "brand-reference", "host-auth"],
30
30
  "frameAncestors": ["'self'", "https://*.vercel.app", "https://app.example.com"],
31
31
  "plugins": { "bot": false, "visitors": false, "brandIntro": false, "botBrowser": false, "providerQuotas": false }
32
32
  }
@@ -62,6 +62,14 @@ POST → { "email": "…", "role": "…", "action": "create|list|list.all|revo
62
62
  - The token is already verified before the call: your route does not receive it and must not
63
63
  re-verify it.
64
64
 
65
+ ⚠️ **Verified against whom?** The player's database and your identity provider are two different
66
+ things. They are the same project while the player and your application share a deployment — and
67
+ different by construction once the instance is separate. Point `PLAYER_AUTH_URL` at the project
68
+ that issues your members' tokens (with its own publishable key in `PLAYER_AUTH_KEY`), or every
69
+ member action is refused in a way that reads like a missing permission. Unset, it falls back to
70
+ the player's own project, so a shared deployment changes by not one character. The `host-auth`
71
+ capability tells you an instance supports the split.
72
+
65
73
  ### 2. What a client's brand is
66
74
 
67
75
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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
@@ -2581,8 +2581,13 @@ async function handler(req, res) {
2581
2581
  contract: 1,
2582
2582
  version: PLAYER_VERSION,
2583
2583
  // Ce que cette instance sait faire. Un hôte teste la présence, jamais l'ordre.
2584
+ // `host-auth` : cette instance sait vérifier les jetons auprès d'un émetteur DISTINCT de
2585
+ // sa base (PLAYER_AUTH_URL). Un hôte tiers en a besoin pour savoir si ses membres
2586
+ // peuvent seulement s'authentifier — sans ça, sa seule voie était d'essayer et de lire
2587
+ // un refus qui ressemble à un droit manquant. Le nom, jamais l'émetteur : la carte reste
2588
+ // muette sur les URL.
2584
2589
  capabilities: [
2585
- "docshare", "presentations", "embed-denied", "host-fetch", "brand-reference",
2590
+ "docshare", "presentations", "embed-denied", "host-fetch", "brand-reference", "host-auth",
2586
2591
  ],
2587
2592
  // ⚠️ POUR QUELLES ORIGINES cette instance accepte d'être encadrée. Un booléen ne
2588
2593
  // suffisait pas : un hôte a besoin de voir que SON domaine manque, pas seulement que