discovery-media-player 0.1.110 → 0.1.111

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.
@@ -69,7 +69,16 @@ function creerDb(env) {
69
69
  // début. Tronqué : c'est un message d'erreur, pas un relais de données.
70
70
  let detail = "";
71
71
  try { detail = (await r.text()).slice(0, 300); } catch { /* le statut restera seul */ }
72
- throw new Error(`Supabase ${methode} ${chemin} → ${r.status}${detail ? ` — ${detail}` : ""}`);
72
+ // ⚠️ LE MESSAGE NE CHANGE PAS — du code appelant y cherche « 409 ». On AJOUTE seulement de quoi
73
+ // décider sans lire une phrase : le statut, et le corps PostgREST analysé quand il l'est. Sans
74
+ // ça, distinguer « cette fonction n'existe pas » (PGRST202) d'un hoquet réseau obligeait à
75
+ // deviner sur du texte — et l'appelant concluait « migration absente » par défaut, ce qui lui
76
+ // faisait retirer une protection sur une simple panne. L'autre contexte (studio) expose déjà
77
+ // `statusCode`/`details` : les deux formes convergent enfin.
78
+ const erreur = new Error(`Supabase ${methode} ${chemin} → ${r.status}${detail ? ` — ${detail}` : ""}`);
79
+ erreur.statusCode = r.status;
80
+ try { erreur.details = JSON.parse(detail); } catch { /* corps non JSON : le message suffit */ }
81
+ throw erreur;
73
82
  }
74
83
  const texte = await r.text();
75
84
  return texte ? JSON.parse(texte) : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.110",
3
+ "version": "0.1.111",
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",
@@ -6,7 +6,7 @@ const crypto = require("crypto");
6
6
  // ⚠️ Le contexte est REÇU, pas construit. Ce module ne doit pas savoir d'où il vient : c'est ce
7
7
  // qui lui permettra de partir dans le dépôt du player sans emporter le studio avec lui.
8
8
  let PLAYER = null;
9
- function init(ctx) { PLAYER = ctx; }
9
+ function init(ctx) { PLAYER = ctx; _bumpSansDurcissementJusqua = 0; _avertRpcPresence = false; }
10
10
 
11
11
 
12
12
  const enc = encodeURIComponent;
@@ -779,10 +779,41 @@ let _avertRpcPresence = false;
779
779
  * aller-retour perdu ; le chemin de la présence est chaud (un battement toutes les 25 s par
780
780
  * participant). On retient donc l'ensemble d'arguments qui a marché, pour le processus.
781
781
  */
782
- let _bumpSansDurcissement = false; // 0018 absente : ne plus demander p_only_if_unclaimed
782
+ /**
783
+ * Cette erreur dit-elle « CETTE SIGNATURE N'EXISTE PAS », et rien d'autre ?
784
+ *
785
+ * ⚠️ C'EST LA QUESTION QUI MANQUAIT, ET SON ABSENCE RETIRAIT UNE PROTECTION. Le repli vers l'ancien
786
+ * contrat se déclenchait sur N'IMPORTE QUELLE exception : un `ECONNRESET`, un 500, un délai dépassé
787
+ * valaient « migration 0018 absente », et le processus restait dégradé — sans contrôle anti-usurpation
788
+ * — jusqu'à son redémarrage. Une panne réseau d'une seconde désarmait une garde de sécurité sur une
789
+ * base pourtant entièrement migrée.
790
+ *
791
+ * C'est la règle du jour appliquée au code de production : **un mécanisme qui ne peut pas mesurer doit
792
+ * refuser de conclure, pas conclure par défaut.** Ici, ne pas savoir distinguer PGRST202 d'un timeout
793
+ * ne rendait pas le repli prudent — il le rendait automatique.
794
+ *
795
+ * PostgREST rend `PGRST202` quand aucune fonction ne correspond au jeu d'arguments nommés. On accepte
796
+ * les DEUX formes que nos contextes produisent (code analysé, ou message contenant le code / la phrase
797
+ * de PostgREST) — et RIEN d'autre : un statut 404 seul ne suffit pas, il peut venir d'ailleurs.
798
+ */
799
+ function signatureAbsente(erreur) {
800
+ if (!erreur) return false;
801
+ const code = erreur.details && (erreur.details.code || (erreur.details.error && erreur.details.error.code));
802
+ if (code === "PGRST202") return true;
803
+ const texte = String((erreur && erreur.message) || "");
804
+ return texte.includes("PGRST202") || /Could not find the function/i.test(texte);
805
+ }
806
+
807
+ // ⚠️ LE MÉMO EXPIRE — UN « NON » N'A PAS LA DURÉE DE VIE D'UN « OUI ». C'est la doctrine que schema.js
808
+ // applique déjà à ses sondes, et elle vaut ici pour la même raison : « 0018 absente » est un état que
809
+ // l'exploitant peut RÉPARER pendant que le processus tourne. Mémorisé pour toujours, il aurait exigé un
810
+ // redémarrage pour que le durcissement reprenne — une migration appliquée serait restée sans effet, en
811
+ // silence. Un « oui » (la signature existe) n'a pas besoin d'expirer : une fonction ne disparaît pas.
812
+ let _bumpSansDurcissementJusqua = 0;
813
+ const MEMO_SANS_DURCISSEMENT_MS = 60 * 1000;
783
814
  async function appelerBump(corps, durcissementVoulu) {
784
815
  const appel = (b) => PLAYER.db.request("rpc/player_attendance_bump", { method: "POST", body: b });
785
- if (durcissementVoulu && _bumpSansDurcissement) {
816
+ if (durcissementVoulu && Date.now() < _bumpSansDurcissementJusqua) {
786
817
  const { p_only_if_unclaimed: _retire, ...sansDurcissement } = corps;
787
818
  return appel(sansDurcissement);
788
819
  }
@@ -790,9 +821,12 @@ async function appelerBump(corps, durcissementVoulu) {
790
821
  return await appel(corps);
791
822
  } catch (erreur) {
792
823
  if (!durcissementVoulu) throw erreur; // rien à retirer : l'échec est réel
824
+ // ⚠️ ON NE SE REPLIE QUE SUR LA PREUVE. Toute autre erreur remonte telle quelle : l'appelant
825
+ // refusera le battement plutôt que de l'écrire sans le contrôle qu'il avait demandé.
826
+ if (!signatureAbsente(erreur)) throw erreur;
793
827
  // La signature à 12 arguments n'existe pas (0018 non appliquée) : on retire l'argument et on
794
828
  // réessaie. Le durcissement n'est alors PAS appliqué — et ça se dit, une fois, plus bas.
795
- _bumpSansDurcissement = true;
829
+ _bumpSansDurcissementJusqua = Date.now() + MEMO_SANS_DURCISSEMENT_MS;
796
830
  // ⚠️ CE QUI DISPARAÎT SE DIT. Le durcissement demandé n'est pas appliqué : sous
797
831
  // PLAYER_PRESENCE_STRICT, la porte se fermerait sur les battements legacy tout en laissant un
798
832
  // bootstrap auto-déclaré s'emparer d'une présence réclamée — c'est-à-dire une fermeture qui
@@ -871,6 +905,21 @@ async function recordAttendance(slug, participant, { presentation = null, ipHash
871
905
  }
872
906
  // Forme inattendue : on n'invente pas de verdict, on laisse la boucle de repli faire l'écriture.
873
907
  } catch (erreur) {
908
+ // ⚠️ UN DURCISSEMENT DEMANDÉ QUI N'A PAS PU S'EXÉCUTER NE SE CONTOURNE PAS. Le repli en boucle
909
+ // lire-modifier-réécrire écrit SANS le contrôle anti-usurpation : l'emprunter ici reviendrait à
910
+ // faire par la porte de service ce que le chemin principal vient de refuser. On rend 503 — « je
911
+ // n'ai pas pu vérifier », pas « c'est refusé » ni « c'est écrit » — et le battement suivant
912
+ // réessaiera dans quelques secondes. Fail-closed sur le contrôle, jamais sur la disponibilité des
913
+ // battements ORDINAIRES, qui continuent de se replier normalement.
914
+ if (onlyIfUnclaimed) {
915
+ try {
916
+ PLAYER.errors.capture(new Error(
917
+ "bootstrap de présence refusé : le contrôle anti-usurpation n'a pas pu s'exécuter — "
918
+ + ((erreur && erreur.message) || erreur),
919
+ ), { route: "present-attend" });
920
+ } catch { /* jamais bloquant */ }
921
+ return { ok: false, status: 503 };
922
+ }
874
923
  if (!_avertRpcPresence) {
875
924
  _avertRpcPresence = true;
876
925
  try {