discovery-media-player 0.1.117 → 0.1.119
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/docs/HOST-CONTRACT.md +11 -0
- package/package.json +1 -1
- package/server/page-audience.js +70 -9
- package/server/presentations.js +39 -18
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -35,6 +35,9 @@ need.
|
|
|
35
35
|
"frameAncestors": ["'self'", "https://*.vercel.app", "https://app.example.com"],
|
|
36
36
|
"separateIssuer": true,
|
|
37
37
|
"internalStrict": true,
|
|
38
|
+
"presenceStrict": true,
|
|
39
|
+
"presenceJetons": true,
|
|
40
|
+
"presenceDurcissement": "inconnu",
|
|
38
41
|
"retentionSweep": false,
|
|
39
42
|
"hostShare": true,
|
|
40
43
|
"hostMail": true,
|
|
@@ -63,6 +66,14 @@ different question when you want a real answer:
|
|
|
63
66
|
GET /api/doc?contract=1&schema=1
|
|
64
67
|
```
|
|
65
68
|
|
|
69
|
+
The three `presence*` fields report what the host has **observed**, not what it is configured to do:
|
|
70
|
+
|
|
71
|
+
| field | meaning |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `presenceJetons` | measured — the host actually signed a throwaway token, so `PLAYER_PRESENCE_SECRET` works |
|
|
74
|
+
| `presenceStrict` | **effective** — `PLAYER_PRESENCE_STRICT` is set *and* tokens can be issued. A closed door announced over an open one would be the worse failure |
|
|
75
|
+
| `presenceDurcissement` | `actif` (a hardened call came back), `degrade` (migration 0018 is missing), `inconnu` (nothing attempted in this process — **not** a green light, and process-local: another instance may have seen otherwise) |
|
|
76
|
+
|
|
66
77
|
That parameter **is** the one part of this card that needs the database, and only when you ask for
|
|
67
78
|
it. `verdict` is then one of:
|
|
68
79
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.119",
|
|
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/page-audience.js
CHANGED
|
@@ -94,6 +94,17 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
|
|
|
94
94
|
(function(){
|
|
95
95
|
var CFG=${cfg};
|
|
96
96
|
var PDF=null, total=0, cur=CFG.page||1, ready=false;
|
|
97
|
+
// ⚠️ DEUX GÉNÉRATIONS, PARCE QU'IL Y A DEUX COURSES DISTINCTES (P1 audit externe).
|
|
98
|
+
//
|
|
99
|
+
// docGen : le présentateur change de document pendant qu'un chargement est en cours. Le
|
|
100
|
+
// callback de l'ANCIEN document arrivait après le nouveau et posait ses pages par-dessus.
|
|
101
|
+
// renderGen : deux getPage() en vol (changement de page rapide, redimensionnement) peuvent
|
|
102
|
+
// se résoudre DANS LE DÉSORDRE — pdf.js ne garantit aucun ordre entre deux pages. Le plus lent
|
|
103
|
+
// gagnait, et l'audience restait sur une page que personne n'avait demandée.
|
|
104
|
+
//
|
|
105
|
+
// Une génération par course : les confondre ferait qu'un simple changement de page invaliderait
|
|
106
|
+
// le document, ou l'inverse.
|
|
107
|
+
var docGen=0, renderGen=0, tacheRendu=null, chargement=null;
|
|
97
108
|
var stage=document.getElementById('stage'), pageEl=document.getElementById('page');
|
|
98
109
|
function hideLoader(){ var l=document.getElementById('load'); if(l){ l.classList.add('hide'); setTimeout(function(){ if(l.parentNode)l.parentNode.removeChild(l); },450);} }
|
|
99
110
|
function ended(){ document.getElementById('ended').style.display='flex';
|
|
@@ -115,7 +126,14 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
|
|
|
115
126
|
pgi.innerHTML=''; pgi.appendChild(el);
|
|
116
127
|
return;
|
|
117
128
|
}
|
|
129
|
+
// ⚠️ ON ANNULE LE RENDU PRÉCÉDENT AVANT D'EN LANCER UN AUTRE. Le RenderTask était jeté :
|
|
130
|
+
// deux rendus peignaient alors en parallèle, et le plus lent finissait par l'emporter.
|
|
131
|
+
if(tacheRendu){ try{ tacheRendu.cancel(); }catch(e){} tacheRendu=null; }
|
|
132
|
+
var monDoc=docGen, monRendu=++renderGen;
|
|
118
133
|
PDF.getPage(n).then(function(page){
|
|
134
|
+
// Le document a changé, ou une page plus récente a été demandée pendant que celle-ci
|
|
135
|
+
// arrivait : ce résultat n'est plus celui qu'on attend. On ne le pose pas.
|
|
136
|
+
if(monDoc!==docGen || monRendu!==renderGen) return;
|
|
119
137
|
var availW=Math.max(120, stage.clientWidth-40), availH=Math.max(120, stage.clientHeight-40);
|
|
120
138
|
var v1=page.getViewport({scale:1});
|
|
121
139
|
var scale=Math.min(availW/v1.width, availH/v1.height); // fit entier → une page à l'écran
|
|
@@ -123,9 +141,18 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
|
|
|
123
141
|
var cv=document.createElement('canvas'); cv.width=Math.floor(vp.width*dpr); cv.height=Math.floor(vp.height*dpr);
|
|
124
142
|
cv.setAttribute('role','img'); cv.setAttribute('aria-label','Page '+n+(total>1?' sur '+total:''));
|
|
125
143
|
cv.style.width=vp.width+'px'; cv.style.height=vp.height+'px';
|
|
126
|
-
page.render({canvasContext:cv.getContext('2d'),viewport:vp,transform:dpr!==1?[dpr,0,0,dpr,0,0]:null});
|
|
127
|
-
|
|
128
|
-
|
|
144
|
+
var t=page.render({canvasContext:cv.getContext('2d'),viewport:vp,transform:dpr!==1?[dpr,0,0,dpr,0,0]:null});
|
|
145
|
+
tacheRendu=t;
|
|
146
|
+
// ⚠️ ON NE PUBLIE QU'APRÈS LE RENDU. L'ancien code posait le canvas AVANT que pdf.js n'ait
|
|
147
|
+
// peint : la page apparaissait blanche puis se remplissait, et un rendu périmé pouvait
|
|
148
|
+
// encore écraser le bon. On revérifie la génération APRÈS l'attente — c'est là qu'elle a pu
|
|
149
|
+
// changer, pas avant.
|
|
150
|
+
return t.promise.then(function(){
|
|
151
|
+
if(monDoc!==docGen || monRendu!==renderGen) return;
|
|
152
|
+
if(tacheRendu===t) tacheRendu=null;
|
|
153
|
+
pageEl.innerHTML=''; pageEl.appendChild(cv);
|
|
154
|
+
});
|
|
155
|
+
}).catch(function(){ /* rendu annulé ou page illisible : le rendu courant fait foi */ });
|
|
129
156
|
}
|
|
130
157
|
function boot(){
|
|
131
158
|
import(CFG.pdfjs).then(function(m){
|
|
@@ -170,35 +197,67 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
|
|
|
170
197
|
// réponse n est jamais « celui que le banc sait voir » : on garde le champ qui fait foi, puis on
|
|
171
198
|
// rend le banc capable de le distinguer — un cas où l URL ment et où seul le nom dit vrai.
|
|
172
199
|
function estImage(){ return Player.viewer.isImageDocument(CFG.fileName, CFG.docUrl); }
|
|
173
|
-
function chargerImage(){
|
|
200
|
+
function chargerImage(monDoc){
|
|
174
201
|
var im=new Image();
|
|
175
202
|
im.alt='Document présenté';
|
|
176
203
|
im.onload=function(){
|
|
204
|
+
// Le présentateur a pu rebasculer pendant le téléchargement : cette image n'est plus celle
|
|
205
|
+
// du document courant.
|
|
206
|
+
if(monDoc!==docGen) return;
|
|
177
207
|
total=1; ready=true; PDF={image:im};
|
|
178
208
|
var tot=document.getElementById('tot'); if(tot)tot.textContent=1;
|
|
179
209
|
show(1); hideLoader();
|
|
180
210
|
if(!CFG.active) ended();
|
|
181
211
|
};
|
|
182
|
-
im.onerror=function(){ var l=document.getElementById('load'); if(l)l.querySelector('.lsub').textContent="Document indisponible."; };
|
|
212
|
+
im.onerror=function(){ if(monDoc!==docGen) return; var l=document.getElementById('load'); if(l)l.querySelector('.lsub').textContent="Document indisponible."; };
|
|
183
213
|
im.src=CFG.fileUrl;
|
|
184
214
|
}
|
|
215
|
+
// ⚠️ ON LIBÈRE L'ANCIEN DOCUMENT. Un PDFDocumentProxy retient ses pages, ses canvas et son
|
|
216
|
+
// worker : enchaîner les bascules sans détruire faisait croître la mémoire sans borne, et c'est
|
|
217
|
+
// le mode de panne d'une présentation longue — celle qui dure, pas celle qu'on teste.
|
|
218
|
+
function libererDocument(){
|
|
219
|
+
if(tacheRendu){ try{ tacheRendu.cancel(); }catch(e){} tacheRendu=null; }
|
|
220
|
+
if(chargement){ try{ chargement.destroy(); }catch(e){} chargement=null; }
|
|
221
|
+
if(PDF && !PDF.image && typeof PDF.destroy==='function'){ try{ PDF.destroy(); }catch(e){} }
|
|
222
|
+
PDF=null;
|
|
223
|
+
}
|
|
185
224
|
function load(){
|
|
186
|
-
|
|
187
|
-
|
|
225
|
+
var monDoc=docGen;
|
|
226
|
+
if(estImage()){ chargerImage(monDoc); return; }
|
|
227
|
+
var t=pdfjsLib.getDocument({url:CFG.fileUrl,isEvalSupported:false});
|
|
228
|
+
chargement=t;
|
|
229
|
+
t.promise.then(function(pdf){
|
|
230
|
+
// Une bascule est survenue pendant le chargement : ce document n'est plus attendu, et le
|
|
231
|
+
// garder fuirait. On le détruit plutôt que de simplement l'ignorer.
|
|
232
|
+
if(monDoc!==docGen){ try{ pdf.destroy(); }catch(e){} return; }
|
|
188
233
|
PDF=pdf; total=pdf.numPages; ready=true;
|
|
189
234
|
var tot=document.getElementById('tot'); if(tot)tot.textContent=total;
|
|
190
235
|
show(cur); hideLoader();
|
|
191
236
|
if(!CFG.active) ended();
|
|
192
|
-
}).catch(function(){ var l=document.getElementById('load'); if(l)l.querySelector('.lsub').textContent="Document indisponible."; });
|
|
237
|
+
}).catch(function(){ if(monDoc!==docGen) return; var l=document.getElementById('load'); if(l)l.querySelector('.lsub').textContent="Document indisponible."; });
|
|
193
238
|
}
|
|
194
239
|
// Le présentateur a changé de document (même session) → recharger le PDF servi par le proxy (URL identique →
|
|
195
240
|
// cache-buster obligatoire), remettre à la page 1, mettre à jour le titre.
|
|
241
|
+
// ⚠️ TOUT CE QUI DÉCRIT LE DOCUMENT CHANGE ENSEMBLE, fileName COMPRIS — c'était le P1.
|
|
242
|
+
//
|
|
243
|
+
// fileName n'était PAS mis à jour, alors que c'est lui qui fait foi pour décider image ou PDF
|
|
244
|
+
// (estImage, et le commentaire plus bas dit pourquoi). Après une bascule, le nom restait celui
|
|
245
|
+
// du document précédent : un PDF→PNG envoyait donc le nouveau PNG à pdf.js, et un PNG→PDF
|
|
246
|
+
// chargeait le nouveau PDF avec new Image(). Dans les deux cas : « Document indisponible ».
|
|
247
|
+
//
|
|
248
|
+
// Une mise à jour partielle d'un état qui se lit comme un TOUT est la forme du défaut : il n'y a
|
|
249
|
+
// pas de moment où CFG doit décrire un document par son URL et un autre par son nom.
|
|
196
250
|
function switchDoc(row){
|
|
251
|
+
docGen++; // invalide tout callback du document précédent
|
|
252
|
+
libererDocument();
|
|
197
253
|
var t=document.getElementById('ptitle'); if(t) t.textContent=row.doc_title||row.file_name||'Document';
|
|
198
254
|
var pg=document.getElementById('page'); if(pg) pg.innerHTML='';
|
|
199
255
|
cur=1; total=0; ready=false;
|
|
200
256
|
var tot=document.getElementById('tot'); if(tot)tot.textContent='—';
|
|
201
257
|
var cu=document.getElementById('cur'); if(cu)cu.textContent='1';
|
|
258
|
+
if(row.file_url) CFG.docUrl=row.file_url;
|
|
259
|
+
CFG.fileName=row.file_name||'';
|
|
260
|
+
CFG.title=row.doc_title||row.file_name||'Document';
|
|
202
261
|
CFG.fileUrl='/api/doc?present='+encodeURIComponent(CFG.slug)+'&file=1&v='+encodeURIComponent(row.updated_at||String(row.current_page||1));
|
|
203
262
|
load();
|
|
204
263
|
}
|
|
@@ -229,7 +288,9 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
|
|
|
229
288
|
if(a.kind==='ended'){ ended(); return; }
|
|
230
289
|
if(a.kind==='show-map'){ if(window.Map3DD){ if(a.content.kind==='streetview') Map3DD.enterSV(a.content,false); else Map3DD.enter(a.content,false); } return; }
|
|
231
290
|
if(a.kind==='leave-map'){ if(window.Map3DD) Map3DD.exit(); }
|
|
232
|
-
|
|
291
|
+
// switchDoc pose LUI-MÊME docUrl/fileName/titre : les poser ici, en partie, était
|
|
292
|
+
// exactement ce qui laissait fileName en arrière.
|
|
293
|
+
if(a.kind==='switch-doc'){ switchDoc(row); return; }
|
|
233
294
|
if(a.kind==='show-page'){ show(a.page); }
|
|
234
295
|
}
|
|
235
296
|
}
|
package/server/presentations.js
CHANGED
|
@@ -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; _bumpSansDurcissementJusqua = 0; _avertRpcPresence = false;
|
|
9
|
+
function init(ctx) { PLAYER = ctx; _bumpSansDurcissementJusqua = 0; _avertRpcPresence = false; _etatDurcissement = "inconnu"; }
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* L'état OBSERVÉ du durcissement des bootstraps — pas sa configuration.
|
|
@@ -23,14 +23,17 @@ function init(ctx) { PLAYER = ctx; _bumpSansDurcissementJusqua = 0; _avertRpcPre
|
|
|
23
23
|
*/
|
|
24
24
|
function etatDurcissementBootstrap() {
|
|
25
25
|
if (Date.now() < _bumpSansDurcissementJusqua) return "degrade";
|
|
26
|
-
// ⚠️
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
26
|
+
// ⚠️ UN ÉTAT EXPLICITE, PAS UNE COMPARAISON D'INSTANTS. La version précédente comparait le dernier
|
|
27
|
+
// succès au dernier échec — correct sur le fond, mais deux réponses concurrentes qui se terminent
|
|
28
|
+
// dans la MÊME milliseconde rendaient les deux instants égaux, et l'égalité tombait du côté
|
|
29
|
+
// « inconnu » : jamais un faux « actif », mais un faux négatif possible. Le dernier mot observé
|
|
30
|
+
// s'écrit ; il n'a pas à se déduire d'une horloge dont la résolution n'est pas garantie.
|
|
31
|
+
// (Simplification proposée par l'audit externe.)
|
|
32
|
+
//
|
|
33
|
+
// ⚠️ ET L'EXPIRATION NE PROMEUT TOUJOURS PAS. Une preuve NÉGATIVE périmée retombe sur l'ignorance,
|
|
34
|
+
// jamais sur la confiance : c'était le défaut de 0.1.115, et le remède ne doit pas le réintroduire
|
|
35
|
+
// en chemin. Un « oui » n'expire pas — une fonction ne disparaît pas toute seule.
|
|
36
|
+
return _etatDurcissement === "degrade" ? "inconnu" : _etatDurcissement;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
|
|
@@ -836,12 +839,11 @@ function signatureAbsente(erreur) {
|
|
|
836
839
|
// silence. Un « oui » (la signature existe) n'a pas besoin d'expirer : une fonction ne disparaît pas.
|
|
837
840
|
let _bumpSansDurcissementJusqua = 0;
|
|
838
841
|
const MEMO_SANS_DURCISSEMENT_MS = 60 * 1000;
|
|
839
|
-
// ⚠️
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
let
|
|
844
|
-
let _dernierEchecDurcissement = 0;
|
|
842
|
+
// ⚠️ LE DERNIER MOT OBSERVÉ, ÉCRIT — pas déduit. Un booléen « on a essayé » ne pouvait pas dire si
|
|
843
|
+
// ce mot fut un succès ou un échec, et c'est lui qui décide. Comparer deux instants le disait, au
|
|
844
|
+
// prix d'une égalité possible à la milliseconde. On écrit donc l'état : « actif » (un appel durci
|
|
845
|
+
// est revenu), « degrade » (la signature manquait), « inconnu » (rien constaté dans ce processus).
|
|
846
|
+
let _etatDurcissement = "inconnu";
|
|
845
847
|
// ⚠️ A-T-ON SEULEMENT ESSAYÉ ? « Pas dégradé » n'est pas « vérifié » : un processus qui vient de
|
|
846
848
|
// démarrer n'a rien tenté, et rendre « actif » reviendrait à annoncer une garde active sur la foi
|
|
847
849
|
// d'une absence d'observation. Trois états, donc — la même règle que le verdict du schéma, où « rien
|
|
@@ -857,7 +859,7 @@ async function appelerBump(corps, durcissementVoulu) {
|
|
|
857
859
|
// ⚠️ ON NE MARQUE LE SUCCÈS QU'ICI — l'appel est REVENU, donc la signature à 12 arguments existe.
|
|
858
860
|
// Un rendu `{ok:false, usurpe:true}` compte : seule 0018 sait répondre ça. Ce qui se mesure est
|
|
859
861
|
// le RETOUR, jamais l'intention de partir.
|
|
860
|
-
if (durcissementVoulu)
|
|
862
|
+
if (durcissementVoulu) _etatDurcissement = "actif";
|
|
861
863
|
return r;
|
|
862
864
|
} catch (erreur) {
|
|
863
865
|
if (!durcissementVoulu) throw erreur; // rien à retirer : l'échec est réel
|
|
@@ -866,8 +868,8 @@ async function appelerBump(corps, durcissementVoulu) {
|
|
|
866
868
|
if (!signatureAbsente(erreur)) throw erreur;
|
|
867
869
|
// La signature à 12 arguments n'existe pas (0018 non appliquée) : on retire l'argument et on
|
|
868
870
|
// réessaie. Le durcissement n'est alors PAS appliqué — et ça se dit, une fois, plus bas.
|
|
869
|
-
|
|
870
|
-
_bumpSansDurcissementJusqua =
|
|
871
|
+
_etatDurcissement = "degrade";
|
|
872
|
+
_bumpSansDurcissementJusqua = Date.now() + MEMO_SANS_DURCISSEMENT_MS;
|
|
871
873
|
// ⚠️ CE QUI DISPARAÎT SE DIT. Le durcissement demandé n'est pas appliqué : sous
|
|
872
874
|
// PLAYER_PRESENCE_STRICT, la porte se fermerait sur les battements legacy tout en laissant un
|
|
873
875
|
// bootstrap auto-déclaré s'emparer d'une présence réclamée — c'est-à-dire une fermeture qui
|
|
@@ -879,6 +881,25 @@ async function appelerBump(corps, durcissementVoulu) {
|
|
|
879
881
|
+ "de jeton — n'armez pas PLAYER_PRESENCE_STRICT avant de l'avoir appliquée.",
|
|
880
882
|
), { route: "present-attend" });
|
|
881
883
|
} catch { /* jamais bloquant */ }
|
|
884
|
+
// ⚠️ SOUS PORTE FERMÉE, ON NE SE REPLIE PAS — ON REFUSE. Le repli retire le contrôle
|
|
885
|
+
// anti-usurpation et écrit quand même : acceptable pendant la TRANSITION (mieux vaut une
|
|
886
|
+
// présence enregistrée sans contrôle qu'une présentation cassée), inacceptable une fois
|
|
887
|
+
// PLAYER_PRESENCE_STRICT posé. À ce moment-là l'exploitant a déclaré que seule une identité
|
|
888
|
+
// prouvée entre ; laisser un bootstrap auto-déclaré s'emparer d'une ligne réclamée serait une
|
|
889
|
+
// fermeture qui rassure sans protéger — précisément ce que la porte prétend empêcher.
|
|
890
|
+
//
|
|
891
|
+
// On lève, et l'appelant rend 503 : « je n'ai pas pu vérifier », pas « c'est refusé » ni « c'est
|
|
892
|
+
// écrit ». ⚠️ Seuls les BOOTSTRAPS sont concernés (durcissementVoulu) : un battement prouvé n'a
|
|
893
|
+
// jamais emprunté ce chemin, donc une présentation en cours ne s'arrête pas. (Audit externe.)
|
|
894
|
+
if (PLAYER && PLAYER.config && PLAYER.config.presenceStrict) {
|
|
895
|
+
const refus = new Error(
|
|
896
|
+
"bootstrap de présence NON durci alors que PLAYER_PRESENCE_STRICT est posé : appliquez "
|
|
897
|
+
+ "supabase/migrations/0018-bootstrap-non-usurpable.sql, ou retirez le mode strict le temps "
|
|
898
|
+
+ "de la migration. Aucune écriture non protégée n'a été faite.",
|
|
899
|
+
);
|
|
900
|
+
refus.code = "durcissement-absent";
|
|
901
|
+
throw refus;
|
|
902
|
+
}
|
|
882
903
|
const { p_only_if_unclaimed: _retire, ...sansDurcissement } = corps;
|
|
883
904
|
return appel(sansDurcissement);
|
|
884
905
|
}
|