discovery-media-player 0.1.131 → 0.1.133

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.131",
3
+ "version": "0.1.133",
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",
@@ -124,6 +124,30 @@ async function retirerFichier(chemin, opts) {
124
124
  // au RESTE du plafond (`min(taille, plafond - examinees)`) pour ne jamais le dépasser. Et on
125
125
  // compte les lignes RENDUES par le DELETE (`return=representation&select=id`), pas les ids
126
126
  // présélectionnés : deux exécutions concurrentes n'annoncent pas deux fois la même suppression.
127
+ /**
128
+ * « Cette table n'existe pas ici » — et RIEN d'autre.
129
+ *
130
+ * ⚠️ UNE CIBLE DE PURGE PEUT LÉGITIMEMENT MANQUER, LE CONTRAT LE DIT. `docs/HOST-CONTRACT.md` :
131
+ * les migrations de débit « ne sont délibérément pas » dans le périmètre de la carte, parce
132
+ * qu'« un hôte peut fournir sa propre capacité `limits`, et sur un tel hôte leur absence est
133
+ * NORMALE, pas un défaut ». Le balayage, lui, purgeait `player_rate_limits` sans condition — donc
134
+ * sur cet hôte-là il levait à la cinquième cible sur huit, et les liens révoqués comme les
135
+ * présentations n'étaient JAMAIS atteints. Enveloppé dans un `catch` qui le classe bénin, il
136
+ * restait armé, silencieux, et partiellement inopérant : le pire des trois états.
137
+ *
138
+ * ⚠️ AUSSI ÉTROIT QUE `signatureAbsente`, ET POUR LA MÊME RAISON. On accepte le code PostgREST de la
139
+ * table introuvable, ou son message — et rien d'autre. Un 404 seul ne suffit pas : il peut venir
140
+ * d'ailleurs, et avaler une vraie panne ferait de ce correctif une purge qui ne purge plus sans le
141
+ * dire, c'est-à-dire exactement ce qu'il corrige.
142
+ */
143
+ function tableAbsente(erreur) {
144
+ if (!erreur) return false;
145
+ const code = erreur.details && (erreur.details.code || (erreur.details.error && erreur.details.error.code));
146
+ if (code === "PGRST205") return true;
147
+ const texte = String(erreur.message || "");
148
+ return texte.includes("PGRST205") || /Could not find the table/i.test(texte);
149
+ }
150
+
127
151
  async function purgerParLots(table, filtre, colId, { dryRun = false, taille = LOT, plafond = PLAFOND } = {}, plafondForce) {
128
152
  if (plafondForce !== undefined) plafond = plafondForce;
129
153
  let examinees = 0, supprimees = 0, tronque = false, curseur = null;
@@ -200,7 +224,15 @@ async function purgerRetention(now, optsBrutes = {}) {
200
224
  rapport.commercial_doc_sessions = await purgerParLots("commercial_doc_sessions", `last_at=lt.${enc(bJournaux)}`, "session_id", opts);
201
225
  rapport.commercial_doc_internal_sessions = await purgerParLots("commercial_doc_internal_sessions", `last_at=lt.${enc(bJournaux)}`, "session_id", opts);
202
226
  rapport.doc_bot_sessions = await purgerParLots("doc_bot_sessions", `last_at=lt.${enc(bJournaux)}`, "id", opts);
203
- rapport.player_rate_limits = await purgerParLots("player_rate_limits", `expires_at=lt.${enc(new Date(now).toISOString())}`, "key", opts);
227
+ // ⚠️ LA SEULE CIBLE QUE LE CONTRAT REND OPTIONNELLE — et le balayage la traitait comme les autres.
228
+ // Son absence est normale sur un hôte qui fournit sa propre capacité `limits` ; elle ne doit donc
229
+ // pas interrompre les trois cibles qui suivent. Sautée, ET DITE : un rapport qui omettrait la
230
+ // ligne se lirait comme « rien à supprimer », ce qui est une autre affirmation.
231
+ rapport.player_rate_limits = await purgerParLots("player_rate_limits", `expires_at=lt.${enc(new Date(now).toISOString())}`, "key", opts)
232
+ .catch((e) => {
233
+ if (!tableAbsente(e)) throw e;
234
+ return { examinees: 0, supprimees: 0, tronque: false, sautee: "table absente — l'hôte fournit ses propres compteurs de débit" };
235
+ });
204
236
 
205
237
  // Liens révoqués : seulement là où la révocation est DATÉE (0013).
206
238
  const dateDispo = await require("./schema").attendue("revocationDatee");
@@ -12,6 +12,16 @@ const init = (ctx) => { PLAYER = ctx; docbot = ctx.plugins.bot; };
12
12
  // chute au bout rend `false` et le dispatch continue. Aucune liste d'actions n'est dupliquée
13
13
  // entre ici et handler (un correctif à deux exemplaires finit par diverger) — et aucun appui
14
14
  // sur res.writableEnded, absent des `res` postiches des bancs comme de certains hôtes.
15
+
16
+ /**
17
+ * Les actions qui AGISSENT SUR UNE SESSION EXISTANTE — c'est-à-dire toutes celles du bloc assistant
18
+ * sauf `bot-start`, qui la crée. La liste vit ici plutôt que dans chaque branche pour que le banc
19
+ * puisse la CONFRONTER à l'aiguillage : une action ajoutée à l'un et pas à l'autre fait rougir.
20
+ */
21
+ const ACTIONS_LIEES_A_UNE_SESSION = new Set([
22
+ "bot-say", "bot-history", "bot-nudge", "bot-book", "bot-contact", "bot-rate", "bot-script",
23
+ ]);
24
+
15
25
  async function traiter(req, res, body, _slug) {
16
26
  if (body.action === "bot-tts") {
17
27
  const jp = (status, obj) => { res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(obj)); };
@@ -101,11 +111,38 @@ async function traiter(req, res, body, _slug) {
101
111
  if (!allowed) return jp(429, { ok: false, error: "rate" });
102
112
  const share = await getShareBySlug(String(body.slug || ""));
103
113
  if (!share || !share.bot_enabled) return jp(404, { ok: false, error: "bot" });
114
+ // ⚠️ UNE SESSION EST LIÉE À SON DOCUMENT — VÉRIFIÉ ICI, POUR TOUTES LES ACTIONS À LA FOIS.
115
+ //
116
+ // Cette vérification vivait DANS chaque action, et trois seulement sur sept la faisaient :
117
+ // `bot-rate`, `bot-script` et `bot-history` (celle-ci ajoutée après coup, en 0.1.131).
118
+ // `bot-say`, `bot-nudge`, `bot-book` et `bot-contact` prenaient le même `sessionId` venu du
119
+ // client sans jamais le rattacher au document demandé.
120
+ //
121
+ // ⚠️ CE QUE ÇA OUVRAIT EST PIRE QUE CE QU'ON VENAIT DE FERMER. `bot-history` LISAIT la
122
+ // conversation d'un autre document ; `bot-say` y ÉCRIT et se fait répondre, `bot-contact`
123
+ // attache un nom, un e-mail et un téléphone à la session d'un autre client. Une porte en
124
+ // lecture et quatre en écriture, dans le même bloc.
125
+ //
126
+ // ⚠️ ET LA LEÇON N'EST PAS « IL EN MANQUAIT QUATRE ». Écrite sept fois, cette garde serait
127
+ // oubliée une huitième au prochain ajout — le défaut n'est pas la ligne absente, c'est que
128
+ // sa présence dépende de la mémoire de celui qui écrit l'action. Elle est donc hissée : une
129
+ // action qui porte une session la prouve, sans que son auteur ait à y penser.
130
+ //
131
+ // Le relevé qui a mené ici vient d'un exploitant : « une vérification que plusieurs actions
132
+ // font chacune pour son compte — il faut compter les ACTIONS, pas les implémentations ».
133
+ // La duplication était homogène, donc invisible à un balayage des variantes ; c'est la
134
+ // COUVERTURE qui divergeait, et rien ne la mesurait. `gardesAgent.test.js` la mesure
135
+ // désormais, en lisant la liste d'actions dans ce fichier même.
136
+ //
137
+ // ⚠️ `bot-start` EST EXCLUE, ET C'EST LA SEULE. Elle CRÉE la session : exiger qu'elle en
138
+ // prouve une fermerait l'assistant à tout le monde.
139
+ if (ACTIONS_LIEES_A_UNE_SESSION.has(body.action)) {
140
+ const liee = await docbot.getSession(String(body.sessionId || ""));
141
+ if (!liee || liee.share_slug !== share.slug) return jp(400, { ok: false, error: "session" });
142
+ }
104
143
  const pages = Math.max(0, Math.min(500, Number(body.pages) || 0));
105
144
  const mobile = body.mobile === 1 || body.mobile === true; // téléphone → messages courts + autoplay steps
106
145
  if (body.action === "bot-rate") { // satisfaction (1-5 étoiles) posée depuis le bloc central du viewer
107
- const sess = await docbot.getSession(String(body.sessionId || ""));
108
- if (!sess || sess.share_slug !== share.slug) return jp(400, { ok: false, error: "session" });
109
146
  // ⚠️ CE REFUS ÉTAIT INATTEIGNABLE, ET C'EST LA MESURE QUI EN PAYAIT LE PRIX.
110
147
  //
111
148
  // const note = Math.max(1, Math.min(5, Number(body.rating) || 0));
@@ -155,8 +192,6 @@ async function traiter(req, res, body, _slug) {
155
192
  //
156
193
  // Les deux voisines avaient la garde ET leur banc ; celle-ci n'avait ni l'une ni
157
194
  // l'autre, et la relecture qui a ajouté les bancs a couvert deux actions sur trois.
158
- const sess = await docbot.getSession(String(body.sessionId || ""));
159
- if (!sess || sess.share_slug !== share.slug) return jp(400, { ok: false, error: "session" });
160
195
  return jp(200, { ok: true, messages: await docbot.listMessages(String(body.sessionId || "")) });
161
196
  }
162
197
  // ⚠️ Même piège : un objet littéral répond à `constructor`. Sans `Object.hasOwn`, une
@@ -168,8 +203,6 @@ async function traiter(req, res, body, _slug) {
168
203
  // Bascule de langue EN COURS de présentation : renvoie le script (traduit ou FR) — le client
169
204
  // remplace sa liste d'étapes et rejoue le message courant dans la nouvelle langue.
170
205
  if (body.action === "bot-script") {
171
- const sess = await docbot.getSession(String(body.sessionId || ""));
172
- if (!sess || sess.share_slug !== share.slug) return jp(400, { ok: false, error: "session" });
173
206
  const sp = docbot.applyPron(await docbot.scriptedPayload(share.doc_id, blang, String(body.sessionId)), await docbot.getProfile(share.bot_profile_id));
174
207
  if (!sp) return jp(400, { ok: false, error: "script" });
175
208
  return jp(200, { ok: true, steps: sp.steps, voiceScript: sp.voice, message: sp.hook, closing: sp.closing, messageSay: sp.hookSay, closingSay: sp.closingSay });