discovery-media-player 0.1.57 → 0.1.58

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.
@@ -31,7 +31,8 @@ need.
31
31
  "separateIssuer": true,
32
32
  "hostShare": true,
33
33
  "hostMail": true,
34
- "plugins": { "bot": false, "visitors": false, "brandIntro": false, "botBrowser": false, "providerQuotas": false }
34
+ "plugins": { "bot": false, "visitors": false, "brandIntro": false, "botBrowser": false, "providerQuotas": false },
35
+ "schema": { "attendues": 3, "sondees": 1, "manquant": [] }
35
36
  }
36
37
  ```
37
38
 
@@ -39,6 +40,18 @@ need.
39
40
  never by order. `plugins` lets you refuse to start when you depend on an optional module this
40
41
  instance does not have.
41
42
 
43
+ ⚠️ **`schema` tells you which migrations this instance is still waiting for.** The player never
44
+ applies migrations — it cannot, it only speaks PostgREST — so it *detects* instead, and a missing
45
+ column makes the feature that needs it **degrade silently**, by design, so as not to break a host
46
+ mid-migration. That silence is the point: an operator whose write ordering and message idempotency
47
+ are both switched off sees an instance that looks perfectly healthy. Each entry names the file to
48
+ apply and the feature that is asleep.
49
+
50
+ ⚠️ **`sondees` is not decoration.** The card **reports** what this process has already asked; it
51
+ never probes, because a diagnostic must answer when the database does not. A freshly started
52
+ process therefore knows nothing, and `manquant: []` on `sondees: 0` means *no question asked yet*,
53
+ not *nothing missing*. Compare `sondees` with `attendues` before you conclude.
54
+
42
55
  ⚠️ **`frameAncestors` matters more than it looks.** A host that is not listed will never see the
43
56
  viewer: the browser blocks the iframe **before any script runs**, so no message can be emitted and
44
57
  the host sees a silence indistinguishable from an unreachable instance. Check that your domain is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.57",
3
+ "version": "0.1.58",
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
@@ -3476,10 +3476,7 @@ async function handler(req, res) {
3476
3476
  // capable d'expédier en son nom : le repli silencieux ouvrirait exactement la porte que
3477
3477
  // la séparation ferme. On nomme le fichier à appliquer et on s'arrête.
3478
3478
  if (atteste) {
3479
- const pret = await require("./schema").aLaColonne(
3480
- "commercial_doc_shares", "attested_recipient_email",
3481
- "supabase/migrations/0001-destinataire-atteste.sql",
3482
- );
3479
+ const pret = await require("./schema").attendue("destinataireAtteste");
3483
3480
  if (!pret) {
3484
3481
  return jd(409, { ok: false, error: "migration", message: "Destinataire attesté indisponible : appliquez supabase/migrations/0001-destinataire-atteste.sql." });
3485
3482
  }
@@ -3824,6 +3821,14 @@ async function handler(req, res) {
3824
3821
  // l'hôte connaît déjà son émetteur — il veut seulement savoir si l'instance le regarde.
3825
3822
  // Dire lequel n'aiderait personne et renseignerait qui sonde.
3826
3823
  separateIssuer: !!(PLAYER.config && PLAYER.config.separateIssuer),
3824
+ // ⚠️ L'ÉTAT DU SCHÉMA, LÀ OÙ ON REGARDE. Une colonne absente était signalée par un
3825
+ // `console.warn`, une fois par processus : sur une fonction serverless, une ligne perdue
3826
+ // dans une sortie que personne n'ouvre tant que tout a l'air de marcher — et « tout a
3827
+ // l'air de marcher » est exactement l'état d'un hôte dont trois protections dorment. Cette
3828
+ // carte est ce qu'un hôte interroge déjà pour épingler sa version ; c'est donc ici.
3829
+ // Elle RAPPORTE ce qui est connu, elle ne sonde pas : un diagnostic ne doit pas tomber en
3830
+ // même temps que ce qu'il diagnostique.
3831
+ schema: require("./schema").etatDuSchema(),
3827
3832
  // « L'hôte peut-il créer un lien en son nom propre ? » — configuré, pas seulement
3828
3833
  // possible. Un hôte qui oublie le secret reçoit un 401 qui ressemble à un droit
3829
3834
  // manquant ; ce booléen le lui dit sans qu'il ait à essayer.
@@ -54,9 +54,7 @@ async function reclaimPresentation(slug, email) {
54
54
  // ⚠️ Et j'ai écrit ce champ sans condition en premier jet, ce qui est exactement le piège que
55
55
  // docs/MIGRATIONS.md décrit : PostgREST rejette le PATCH ENTIER si la colonne manque. La reprise
56
56
  // aurait cessé de fonctionner chez tout hôte non migré — pas la nouvelle garantie, la reprise.
57
- const rangDispo = await require("./schema").aLaColonne(
58
- "doc_presentations", "write_seq", "supabase/migrations/0002-ordre-des-ecritures.sql",
59
- );
57
+ const rangDispo = await require("./schema").attendue("rangEcriture");
60
58
  await PLAYER.db.request(`doc_presentations?slug=eq.${enc(slug)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { control_hash: sha(control), active: true, ...(rangDispo ? { write_seq: 0 } : {}), last_seen: new Date().toISOString(), updated_at: new Date().toISOString() } });
61
59
  return { ok: true, slug, control, page: row.current_page || 1, fileUrl: row.file_url, fileName: row.file_name, docTitle: row.doc_title, docId: row.doc_id };
62
60
  }
@@ -200,9 +198,7 @@ async function getPresentation(slug) {
200
198
  async function rangAccepte(row, seq) {
201
199
  const rang = Number(seq);
202
200
  if (!Number.isFinite(rang) || rang <= 0) return { controle: false }; // client plus ancien : pas de rang
203
- const dispo = await require("./schema").aLaColonne(
204
- "doc_presentations", "write_seq", "supabase/migrations/0002-ordre-des-ecritures.sql",
205
- );
201
+ const dispo = await require("./schema").attendue("rangEcriture");
206
202
  if (!dispo) return { controle: false };
207
203
  if (rang <= Number(row.write_seq || 0)) return { controle: true, perime: true };
208
204
  // ⚠️ ON REND LE RANG, PAS UN OBJET TOUT FAIT. La première version rendait « { write_seq: rang } »,
@@ -428,8 +424,7 @@ async function addMessage(slug, { name, email, avatar, isPresenter, isMember, bo
428
424
  // colonne inconnue : chez un hôte non migré, ce n'est pas l'idempotence qu'on perdrait, c'est
429
425
  // l'envoi de messages. Même piège que le rang d'écriture, même sonde.
430
426
  const cle = String(clientKey || "").slice(0, 80);
431
- if (cle && await require("./schema").aLaColonne(
432
- "doc_presentation_messages", "client_key", "supabase/migrations/0005-envoi-unique.sql")) {
427
+ if (cle && await require("./schema").attendue("envoiUnique")) {
433
428
  row.client_key = cle;
434
429
  }
435
430
 
package/server/schema.js CHANGED
@@ -16,6 +16,34 @@
16
16
  // chantiers ont été repoussés pour cette seule raison. Avec cette sonde, l'ordre de déploiement
17
17
  // cesse d'être un piège.
18
18
 
19
+ /**
20
+ * CE QUE CE CODE ATTEND DE LA BASE — DÉCLARÉ UNE FOIS, ET C'EST LA SOURCE.
21
+ *
22
+ * ⚠️ UN INVENTAIRE QUI N'EST PAS LA SOURCE DÉRIVE. Ces couples vivaient recopiés sur quatre
23
+ * appels ; en tirer une simple liste « pour l'affichage » aurait refait, en plus petit, le défaut
24
+ * qui a vidé supabase/init.sql de ses cinq migrations : deux exemplaires du même fait, personne
25
+ * pour les confronter. Les appelants passent donc par `attendue(nom)` et ne nomment plus de
26
+ * colonne — il n'existe plus qu'un endroit où se tromper. Une étape de la forge vérifie en outre
27
+ * que chaque fichier nommé ici existe, et qu'aucun appel ne contourne cette table.
28
+ */
29
+ const ATTENDUES = {
30
+ destinataireAtteste: {
31
+ table: "commercial_doc_shares", colonne: "attested_recipient_email",
32
+ migration: "supabase/migrations/0001-destinataire-atteste.sql",
33
+ fonction: "attribuer une lecture au destinataire attesté par l'hôte",
34
+ },
35
+ rangEcriture: {
36
+ table: "doc_presentations", colonne: "write_seq",
37
+ migration: "supabase/migrations/0002-ordre-des-ecritures.sql",
38
+ fonction: "refuser une écriture de pilotage doublée en vol",
39
+ },
40
+ envoiUnique: {
41
+ table: "doc_presentation_messages", colonne: "client_key",
42
+ migration: "supabase/migrations/0005-envoi-unique.sql",
43
+ fonction: "empêcher qu'un renvoi crée un second message",
44
+ },
45
+ };
46
+
19
47
  let PLAYER = null;
20
48
  /**
21
49
  * Une question posée une fois, retenue pour le processus.
@@ -28,9 +56,59 @@ let PLAYER = null;
28
56
  */
29
57
  const connues = new Map();
30
58
 
59
+ /**
60
+ * Les réponses DÉJÀ OBTENUES, pour qui veut les lire — la carte d'identité, essentiellement.
61
+ *
62
+ * ⚠️ CE N'EST PAS UN DOUBLON DE `connues`. Celle-ci porte des promesses, dont on ne peut rien dire
63
+ * sans les attendre ; celle-là porte des réponses. La distinction compte parce que la carte
64
+ * d'identité ne doit RIEN demander à la base — elle doit répondre quand la base ne répond plus.
65
+ */
66
+ const reponses = new Map();
67
+
31
68
  function init(ctx) {
32
69
  PLAYER = ctx;
33
70
  connues.clear();
71
+ reponses.clear();
72
+ }
73
+
74
+ /** La sonde d'une attente déclarée. C'est la seule forme d'appel que les appelants utilisent. */
75
+ function attendue(nom) {
76
+ const a = ATTENDUES[nom];
77
+ // Un nom inconnu est une faute de frappe, pas une dégradation : la taire ferait passer la
78
+ // fonction pour « en attente de migration » alors qu'elle est simplement mal câblée.
79
+ if (!a) throw new Error(`attente de schéma inconnue : ${nom}`);
80
+ return aLaColonne(a.table, a.colonne, a.migration);
81
+ }
82
+
83
+ /**
84
+ * ⚠️ CE QUE LE JOURNAL NE DIRA JAMAIS À PERSONNE.
85
+ *
86
+ * La sonde signale une colonne absente par un `console.warn`, une fois par processus. Sur une
87
+ * fonction serverless, c'est une ligne perdue dans une sortie que personne n'ouvre quand tout a
88
+ * l'air de marcher — et « tout a l'air de marcher » est précisément l'état d'un hôte dont trois
89
+ * protections dorment. Remarque du second hôte, et elle est juste : la trace existait à l'endroit
90
+ * exact où on ne regarde pas.
91
+ *
92
+ * ⚠️ ON NE SONDE PAS ICI, ON RAPPORTE. La carte d'identité doit répondre quand la base ne répond
93
+ * plus ; sonder depuis elle en ferait un diagnostic qui tombe en même temps que ce qu'il diagnostique.
94
+ *
95
+ * ⚠️ D'OÙ TROIS ÉTATS, ET PAS DEUX. Un processus qui n'a encore rien demandé ne sait rien — et
96
+ * « rien de manquant » se lirait « tout va bien ». Une absence de résultat ressemble à un
97
+ * résultat ; `sondees` est là pour qu'on ne puisse pas les confondre.
98
+ */
99
+ function etatDuSchema() {
100
+ const manquant = [];
101
+ for (const [, r] of reponses) if (!r.present) manquant.push({ migration: r.migration, fonction: r.fonction });
102
+ return {
103
+ attendues: Object.keys(ATTENDUES).length,
104
+ sondees: reponses.size,
105
+ // ⚠️ ON NOMME LE FICHIER, alors que cette route est publique. Même raison que `frameAncestors`
106
+ // juste au-dessus d'elle : l'exploitant n'a AUCUN autre moyen d'apprendre laquelle manque, et
107
+ // un compte nu le laisserait deviner. Ce qu'on révèle en échange — qu'une fonction de
108
+ // fiabilité est en attente, dans un dépôt dont les migrations sont publiques — n'ouvre aucun
109
+ // accès : il faut déjà détenir un jeton de pilotage pour tirer parti d'un rang absent.
110
+ manquant,
111
+ };
34
112
  }
35
113
 
36
114
  /**
@@ -67,6 +145,7 @@ async function sonder(table, colonne, migration, cle) {
67
145
  // portage a un fichier à réécrire, pas une habitude à retrouver partout.
68
146
  const champ = encodeURIComponent(colonne);
69
147
  await PLAYER.db.request(`${table}?select=${champ}&limit=0`);
148
+ noter(cle, true, migration);
70
149
  return true;
71
150
  } catch {
72
151
  // ⚠️ ON NE DISTINGUE PAS « COLONNE ABSENTE » DE « BASE INJOIGNABLE », ET C'EST VOULU. Les deux
@@ -74,11 +153,18 @@ async function sonder(table, colonne, migration, cle) {
74
153
  // message d'erreur, c'est-à-dire de dépendre du texte d'un service tiers. Ce qui change entre
75
154
  // les deux, c'est la durée : une base injoignable le redevient, et le processus suivant reposera
76
155
  // la question.
156
+ noter(cle, false, migration);
77
157
  signaler(cle, migration);
78
158
  return false;
79
159
  }
80
160
  }
81
161
 
162
+ /** La réponse, retenue pour qui la demandera — sans repasser par la base. */
163
+ function noter(cle, present, migration) {
164
+ const a = Object.values(ATTENDUES).find((x) => `${x.table}.${x.colonne}` === cle);
165
+ reponses.set(cle, { present, migration, fonction: a ? a.fonction : "" });
166
+ }
167
+
82
168
  /**
83
169
  * ⚠️ ON NOMME LE FICHIER, PAS L'ERREUR. « column does not exist » envoie l'exploitant lire du
84
170
  * PostgREST ; « appliquez supabase/migrations/0001-…sql » lui dit quoi faire. La différence entre
@@ -92,6 +178,6 @@ function signaler(cle, migration) {
92
178
  }
93
179
 
94
180
  /** Pour les tests et l'exploitation : reposer la question. */
95
- function oublier() { connues.clear(); }
181
+ function oublier() { connues.clear(); reponses.clear(); }
96
182
 
97
- module.exports = { init, aLaColonne, oublier };
183
+ module.exports = { init, aLaColonne, attendue, etatDuSchema, oublier, ATTENDUES };