discovery-media-player 0.1.36 → 0.1.38

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
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
@@ -2577,7 +2577,22 @@ async function handler(req, res) {
2577
2577
  try {
2578
2578
  if (!body.slug || !visitor || !visitor.email) return;
2579
2579
  await PLAYER.db.request("xp_visitor_unlocks", { method: "POST", headers: { Prefer: "return=minimal" }, body: [{ doc_slug: String(body.slug), email: visitor.email, name: visitor.name || null, method }] });
2580
- } catch { /* best-effort */ }
2580
+ } catch (e) {
2581
+ // ⚠️ TROUVÉ EN VÉRIFIANT LE TARBALL DE 0.1.37, PAS PAR LA GARDE QUI VENAIT DE NAÎTRE.
2582
+ //
2583
+ // Celle-ci filtrait sur une LISTE DE NOMS de fonctions d'écriture. `recordUnlock` écrit
2584
+ // directement par `PLAYER.db.request`, donc elle ne l'a pas vu — exactement le défaut que
2585
+ // l'audit reprochait à la garde des prototypes, reproduit dans une garde écrite le même jour
2586
+ // pour éviter ce genre de chose.
2587
+ //
2588
+ // La garde vise désormais la FORME (toute écriture en base rattrapée en silence), pas des
2589
+ // noms. Une liste ne voit que ce qu'on y a mis ; une forme voit aussi le prochain.
2590
+ try {
2591
+ if (await PLAYER.limits.allow("unlock:echec", 1, 3600)) {
2592
+ PLAYER.errors.capture(new Error(`déverrouillage visiteur non journalisé : ${e && e.message ? e.message : "cause inconnue"}`), { route: "visitor-unlock" });
2593
+ }
2594
+ } catch { /* un journal ne doit jamais empêcher une lecture */ }
2595
+ }
2581
2596
  };
2582
2597
  if (body.action === "visitor-google") {
2583
2598
  const r = await V.verifyGoogle(body.credential);
@@ -3139,7 +3154,29 @@ async function handler(req, res) {
3139
3154
  userName: jeton ? (jeton.name || body.name) : body.name,
3140
3155
  numPages: body.numPages, maxPage: body.maxPage, totalSeconds: body.totalSeconds, pagesTime: body.pagesTime,
3141
3156
  }, { ip: ip0, ua: ua0 });
3142
- } catch { /* best-effort */ }
3157
+ } catch (e) {
3158
+ // ⚠️ CE `catch` A COÛTÉ UNE JOURNÉE À UN HÔTE, ET IL ÉTAIT À TROIS LIGNES DU COMMENTAIRE
3159
+ // QUI DIT DE NE PAS FAIRE ÇA.
3160
+ //
3161
+ // La ligne écrite portait `ua` et `ip` ; la table des sessions internes ne les a pas.
3162
+ // PostgREST refusait, ce `catch` avalait le refus, et la route répondait `{"ok":true}`.
3163
+ // Ni le second hôte ni nous n'avions une seule ligne mesurée — notre table de production
3164
+ // en comptait ZÉRO — et rien, nulle part, ne le disait.
3165
+ //
3166
+ // « best-effort » est une intention juste : une mesure ne doit jamais empêcher de lire un
3167
+ // document. Mais best-effort ne veut pas dire MUET. Ce qu'on rattrape ici, c'est le droit
3168
+ // de continuer — pas le droit de ne rien dire.
3169
+ //
3170
+ // ⚠️ La leçon dépasse ce bloc : une règle écrite dans un commentaire ne protège pas le
3171
+ // code qui la suit. C'est pourquoi elle est aussi devenue un test —
3172
+ // server/__tests__/ecritureMuette.test.js refuse tout `catch` vide autour d'une écriture
3173
+ // de mesure.
3174
+ try {
3175
+ if (await PLAYER.limits.allow("intsess:echec", 1, 3600)) {
3176
+ PLAYER.errors.capture(new Error(`écriture de session interne refusée : ${e && e.message ? e.message : "cause inconnue"} — la mesure ne s'enregistre pas`), { route: "internal-session" });
3177
+ }
3178
+ } catch { /* un journal ne doit jamais empêcher une lecture */ }
3179
+ }
3143
3180
  res.statusCode = 200; res.setHeader("Content-Type", "application/json"); res.end('{"ok":true}');
3144
3181
  return;
3145
3182
  }
@@ -3149,7 +3186,19 @@ async function handler(req, res) {
3149
3186
  // 'session' = résumé riche (temps par page, appareil) → upsert ; open/page/heartbeat → journal léger (funnel/overview).
3150
3187
  if (body.event === "session") await upsertSession(share, { sessionId: body.sessionId, numPages: body.numPages, maxPage: body.maxPage, totalSeconds: body.totalSeconds, pagesTime: body.pagesTime }, { ip: ip0, ua: ua0 });
3151
3188
  else await logView(share, { event: body.event, page: body.page, maxPage: body.maxPage, seconds: body.seconds, sessionId: body.sessionId, ua: ua0 });
3152
- } catch { /* best-effort */ }
3189
+ } catch (e) {
3190
+ // ⚠️ LE MÊME `catch` MUET, SUR LE CHEMIN EXTERNE. Trouvé par la garde écrite pour
3191
+ // l'interne : une fois la règle devenue un test, elle a désigné son jumeau.
3192
+ //
3193
+ // Celui-ci n'a rien cassé jusqu'ici — la table des sessions externes a bien les colonnes
3194
+ // qu'on lui envoie. Mais il aurait avalé le prochain écart de la même façon, et personne
3195
+ // n'aurait rien vu : c'est ce qui rend la classe dangereuse, pas l'instance.
3196
+ try {
3197
+ if (await PLAYER.limits.allow("sess:echec", 1, 3600)) {
3198
+ PLAYER.errors.capture(new Error(`écriture de mesure refusée : ${e && e.message ? e.message : "cause inconnue"} — la lecture n'est pas comptée`), { route: "track" });
3199
+ }
3200
+ } catch { /* un journal ne doit jamais empêcher une lecture */ }
3201
+ }
3153
3202
  }
3154
3203
  res.statusCode = 200; res.setHeader("Content-Type", "application/json"); res.end('{"ok":true}');
3155
3204
  return;
package/server/shares.js CHANGED
@@ -344,7 +344,11 @@ async function setShareAuth(slug, requireAuth) {
344
344
  // signalé au point d'entrée plutôt qu'ici.
345
345
  const BORNES = { pages: 10_000, secondes: 24 * 3600, entreesPagesTime: 2_000 };
346
346
 
347
- async function upsertInternalSession(p, { ip, ua }) {
347
+ // ⚠️ `ip` N'EST PLUS LU, ET LA SIGNATURE LE DIT. L'appelant continue de le passer — il ne sait pas
348
+ // ce que chaque table conserve, et ce n'est pas à lui de le savoir. Mais le garder en paramètre
349
+ // nommé laisserait croire qu'il sert : c'est comme ça qu'une donnée revient dans une ligne où elle
350
+ // n'a rien à faire. Une lecture interne, c'est un collègue ; on ne conserve pas son adresse.
351
+ async function upsertInternalSession(p, { ip: _ip, ua }) {
348
352
  const num = (v) => (Number.isFinite(+v) ? Math.trunc(+v) : null);
349
353
  const borne = (v, max) => { const n = num(v); return n == null ? null : Math.max(0, Math.min(n, max)); };
350
354
  const sessionId = String(p.sessionId || "").slice(0, 64);
@@ -390,7 +394,24 @@ async function upsertInternalSession(p, { ip, ua }) {
390
394
  }
391
395
  return out;
392
396
  })(),
393
- ua: String(ua || "").slice(0, 300), ip: String(ip || "").slice(0, 60), device, os, browser, last_at: new Date().toISOString(),
397
+ // ⚠️ NI `ua` NI `ip` ICI, ET C'EST LE CORRECTIF — PAS UNE COLONNE À AJOUTER.
398
+ //
399
+ // Ces deux champs partaient vers une table qui ne les a pas. PostgREST refusait l'insertion
400
+ // (« column "ua" ... does not exist »), le `catch` de l'appelant avalait le refus, et la route
401
+ // répondait `{"ok":true}`. Le suivi interne n'a donc JAMAIS rien écrit — ni chez le second
402
+ // hôte, ni chez nous : notre table de production comptait zéro ligne.
403
+ //
404
+ // ⚠️ Le schéma avait raison, c'est le code qui mentait. Une lecture INTERNE, c'est un collègue :
405
+ // `device`, `os` et `browser` — dérivés — suffisent, et on ne conserve pas l'agent complet ni
406
+ // l'adresse de ses propres équipes. La table des sessions EXTERNES les porte, elle, parce que
407
+ // ce n'est ni la même population ni la même promesse.
408
+ //
409
+ // Corriger en AJOUTANT les colonnes aurait fait l'inverse : mettre le schéma au niveau du code
410
+ // au lieu du code au niveau de l'intention. Ce qu'on garde sur ses propres équipes ne se décide
411
+ // pas par un message d'erreur PostgREST.
412
+ //
413
+ // Trouvé par le second hôte, vérifié en rejouant l'insertion.
414
+ device, os, browser, last_at: new Date().toISOString(),
394
415
  };
395
416
  await PLAYER.db.request("commercial_doc_internal_sessions?on_conflict=session_id", { method: "POST", headers: { Prefer: "resolution=merge-duplicates,return=minimal" }, body: [row] });
396
417
  }