discovery-media-player 0.1.124 → 0.1.126
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/context/standalone.js +9 -1
- package/context/storage.js +14 -0
- package/docs/README.md +8 -0
- package/package.json +12 -4
- package/server/page-visionneuse.js +45 -9
- package/types/context.d.ts +154 -0
- package/types/index.d.ts +41 -0
- package/types/standalone.d.ts +29 -0
package/context/standalone.js
CHANGED
|
@@ -633,7 +633,15 @@ function createStandaloneContext(env = process.env) {
|
|
|
633
633
|
*
|
|
634
634
|
* ⚠️ Sert à fabriquer les liens qui partent par email. L'en-tête `Host` ne convient pas :
|
|
635
635
|
* il est choisi par le client, donc un lecteur pouvait faire envoyer un message signé de
|
|
636
|
-
* l'hôte dont le bouton pointe ailleurs.
|
|
636
|
+
* l'hôte dont le bouton pointe ailleurs.
|
|
637
|
+
*
|
|
638
|
+
* ⚠️ VIDE ⇒ RIEN NE PART. Ce commentaire a annoncé « repli sur `Host`, mais signalé »
|
|
639
|
+
* pendant tout le temps où le code refusait déjà l'envoi (`public-url-unconfigured`,
|
|
640
|
+
* routes-liens.js) — la 0.1.21 posait ce repli, la seconde passe d'audit l'a fermé, et
|
|
641
|
+
* trois textes sur quatre ont continué à le promettre (troisième audit externe, 21/08).
|
|
642
|
+
* Ce getter rend "" ; son unique consommateur retient le courrier, journalise le motif,
|
|
643
|
+
* et crée quand même le lien. Un commentaire qui décrit un comportement retiré est pire
|
|
644
|
+
* qu'une absence de commentaire : il fait renoncer à vérifier.
|
|
637
645
|
*/
|
|
638
646
|
get publicUrl() {
|
|
639
647
|
const v = sansBarreFinale(env.PLAYER_PUBLIC_URL).trim();
|
package/context/storage.js
CHANGED
|
@@ -191,6 +191,20 @@ async function lireDepuis(fh, stat, cible, range) {
|
|
|
191
191
|
return reponse(413, {}, Buffer.alloc(0));
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// ⚠️ UN FICHIER VIDE N'A PAS DE PLAGE À DIFFUSER — et c'est une régression que le passage au flux
|
|
195
|
+
// a introduite. Avec `total === 0`, la borne haute vaut `total - 1`, soit -1 : `createReadStream`
|
|
196
|
+
// reçoit alors `{ start: 0, end: -1 }` et meurt en `TypeError` à la fermeture du descripteur.
|
|
197
|
+
// Le tampon d'avant tolérait `Buffer.alloc(0)` sans rien dire ; le flux, non.
|
|
198
|
+
//
|
|
199
|
+
// Un PDF de zéro octet est évidemment invalide — mais il doit produire une RÉPONSE, pas une
|
|
200
|
+
// exception serveur : c'est le lecteur qui décidera qu'il n'y a rien à afficher, et un 500 lui
|
|
201
|
+
// dirait « notre faute » au lieu de « ce fichier est vide ». Le cas AVEC `Range` est déjà traité
|
|
202
|
+
// plus haut : `debut < total` est faux, donc 416. (Relevé par un audit externe.)
|
|
203
|
+
if (total === 0) {
|
|
204
|
+
await fh.close().catch(() => {});
|
|
205
|
+
return reponse(200, { "content-type": type, "content-length": "0" }, Buffer.alloc(0));
|
|
206
|
+
}
|
|
207
|
+
|
|
194
208
|
// ⚠️ ON DIFFUSE, ON N'ALLOUE PLUS — et le plafond ne suffisait pas à rendre l'allocation sûre.
|
|
195
209
|
//
|
|
196
210
|
// `Buffer.alloc(fin - debut + 1)` réservait la plage DEMANDÉE en une fois : jusqu'à 60 Mio par
|
package/docs/README.md
CHANGED
|
@@ -19,6 +19,14 @@ no document assumes you have read the others.
|
|
|
19
19
|
| [`MIGRATIONS.md`](MIGRATIONS.md) | What happens to a database **already in service** when the player expects a newer schema. (French.) |
|
|
20
20
|
| [`RETENTION.md`](RETENTION.md) | The declared perimeter of data retention: every personal-data column has a written policy, and CI enforces that the list is complete. Also an export of the package: `require.resolve("discovery-media-player/retention")`. (French.) |
|
|
21
21
|
|
|
22
|
+
## You are contributing, or publishing a version
|
|
23
|
+
|
|
24
|
+
| Document | What it gives you |
|
|
25
|
+
|---|---|
|
|
26
|
+
| [`../CONTRIBUTING.md`](../CONTRIBUTING.md) | how to run the benches, what review looks for, and the one rule: a behaviour worth keeping is worth a test that fails without it. |
|
|
27
|
+
| [`../AGENTS.md`](../AGENTS.md) | the conventions that are not obvious from the file tree — which ones a guard enforces, and which ones only review does. |
|
|
28
|
+
| [`RELEASING.md`](RELEASING.md) | the release train, freezing the candidate SHA, the read-only preflight to run **before** the tag, and what to do when a tag lands on the wrong commit. |
|
|
29
|
+
|
|
22
30
|
## You are evaluating the project
|
|
23
31
|
|
|
24
32
|
The external audit trail is public, unedited, and kept in the state it was received —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.126",
|
|
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",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"dist",
|
|
33
33
|
"server",
|
|
34
34
|
"supabase",
|
|
35
|
+
"types",
|
|
35
36
|
"README.md",
|
|
36
37
|
"LICENSE",
|
|
37
38
|
"LICENSE-MIT",
|
|
@@ -40,13 +41,19 @@
|
|
|
40
41
|
"!**/__tests__"
|
|
41
42
|
],
|
|
42
43
|
"exports": {
|
|
43
|
-
".":
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./types/index.d.ts",
|
|
46
|
+
"default": "./server/handler.js"
|
|
47
|
+
},
|
|
44
48
|
"./shares": "./server/shares.js",
|
|
45
49
|
"./presentations": "./server/presentations.js",
|
|
46
50
|
"./brands": "./server/brands.js",
|
|
47
51
|
"./contrat": "./docs/HOST-CONTRACT.md",
|
|
48
52
|
"./retention": "./docs/RETENTION.md",
|
|
49
|
-
"./context/standalone":
|
|
53
|
+
"./context/standalone": {
|
|
54
|
+
"types": "./types/standalone.d.ts",
|
|
55
|
+
"default": "./context/standalone.js"
|
|
56
|
+
},
|
|
50
57
|
"./context/storage": "./context/storage.js",
|
|
51
58
|
"./bridge": {
|
|
52
59
|
"types": "./dist/bridge.d.ts",
|
|
@@ -67,7 +74,8 @@
|
|
|
67
74
|
"prepublishOnly": "npm run build && npm test",
|
|
68
75
|
"test:e2e": "node -e \"require('fs').existsSync('vitest.e2e.config.mjs')||(console.error('Ce banc ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && vitest run --config vitest.e2e.config.mjs",
|
|
69
76
|
"test:base": "node -e \"require('fs').existsSync('vitest.base.config.mjs')||(console.error('Ce banc ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && vitest run --config vitest.base.config.mjs",
|
|
70
|
-
"test:charge": "node -e \"require('fs').existsSync('vitest.charge.config.mjs')||(console.error('Ce banc ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && vitest run --config vitest.charge.config.mjs"
|
|
77
|
+
"test:charge": "node -e \"require('fs').existsSync('vitest.charge.config.mjs')||(console.error('Ce banc ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && vitest run --config vitest.charge.config.mjs",
|
|
78
|
+
"test:campagne": "node -e \"require('fs').existsSync('vitest.campagne.config.mjs')||(console.error('Ce banc ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && vitest run --config vitest.campagne.config.mjs"
|
|
71
79
|
},
|
|
72
80
|
"engines": {
|
|
73
81
|
"node": ">=22"
|
|
@@ -322,6 +322,11 @@ ${LEGAL_CSS}
|
|
|
322
322
|
var taches={}; // n -> RenderTask, pour pouvoir ANNULER
|
|
323
323
|
var enCours={}; // n -> 1 tant que la page n a pas abouti
|
|
324
324
|
var MARGE_PAGES=2; // on garde la page courante et deux de chaque cote
|
|
325
|
+
// ATTENTION : UNE GENERATION PAR PAGE. La generation globale (renderGen) ne bouge qu au build ;
|
|
326
|
+
// une page liberee PENDANT que son rendu ou sa couche texte est en vol gardait donc la meme
|
|
327
|
+
// generation, et le resultat tardif se posait sur une page remise en reserve. On incremente
|
|
328
|
+
// donc a chaque liberation, et chaque callback verifie SA page.
|
|
329
|
+
var genPage=Object.create(null);
|
|
325
330
|
var DPR_MAX=2; // au-dela, le gain visuel est nul et le cout quadratique
|
|
326
331
|
var PIXELS_MAX=4e6; // plafond par canvas : ~4 M pixels = ~16 Mo de tampon
|
|
327
332
|
var onePage=false, soloOffered=false; // mode « une seule page » (présentation guidée) + offre de découverte solo
|
|
@@ -754,6 +759,7 @@ ${LEGAL_CSS}
|
|
|
754
759
|
// Rend une page a l etat de reserve : on retire le canvas et la couche texte, on remet la boite
|
|
755
760
|
// avec sa hauteur estimee. Le defilement ne bouge pas, et la page se re-rendra si on y revient.
|
|
756
761
|
function libererPage(n){
|
|
762
|
+
genPage[n]=(genPage[n]||0)+1; // invalide tout callback en vol pour CETTE page
|
|
757
763
|
var el=pagesEl.querySelector('.page[data-p="'+n+'"]'); if(!el)return;
|
|
758
764
|
if(taches[n]){ try{ taches[n].cancel(); }catch(e){} delete taches[n]; }
|
|
759
765
|
var w=Math.round(targetWidth());
|
|
@@ -766,10 +772,25 @@ ${LEGAL_CSS}
|
|
|
766
772
|
// passer le cas qui fait tomber l onglet. On garde donc la fenetre courante, et on evince
|
|
767
773
|
// au-dela — le nombre de canvas reste borne quel que soit le parcours du document.
|
|
768
774
|
function evincerLoin(centre){
|
|
769
|
-
|
|
770
|
-
|
|
775
|
+
// ATTENTION : LE MODE UNE-PAGE EVINCAIT ZERO, ET MON COMMENTAIRE DISAIT POURQUOI — A TORT.
|
|
776
|
+
//
|
|
777
|
+
// Il portait « une seule page affichee : rien a evincer ». Une seule est AFFICHEE, mais
|
|
778
|
+
// showPage() rend la page courante ET la suivante a chaque navigation, et toutes les
|
|
779
|
+
// precedentes restent dans le DOM. Une presentation guidee de 100 pages finissait donc avec
|
|
780
|
+
// pres de 100 canvas — exactement le defaut que la fenetre glissante ferme ailleurs.
|
|
781
|
+
// Un raisonnement faux ecrit avec assurance fait passer un defaut pour une intention.
|
|
782
|
+
// (Releve par un audit externe.)
|
|
783
|
+
var marge = onePage ? 1 : MARGE_PAGES; // une-page : courante, suivante, precedente
|
|
784
|
+
// ATTENTION : ON PARCOURT L UNION DES TROIS REGISTRES. rendered seul ignore ce qui est EN
|
|
785
|
+
// VOL : une page eloignee dont le getPage n est pas resolu continuait a travailler, et sa
|
|
786
|
+
// couche texte pouvait se poser plus tard sur une page deja liberee.
|
|
787
|
+
var vus = Object.create(null);
|
|
788
|
+
for(var a in rendered) vus[a]=1;
|
|
789
|
+
for(var b in enCours) vus[b]=1;
|
|
790
|
+
for(var c in taches) vus[c]=1;
|
|
791
|
+
for(var k in vus){
|
|
771
792
|
var n=+k;
|
|
772
|
-
if(n<centre-
|
|
793
|
+
if(n<centre-marge || n>centre+marge) libererPage(n);
|
|
773
794
|
}
|
|
774
795
|
}
|
|
775
796
|
function renderPage(n,el){
|
|
@@ -778,7 +799,7 @@ ${LEGAL_CSS}
|
|
|
778
799
|
// donc un echec de getPage ou de render laissait la page marquee pour toujours : elle ne se
|
|
779
800
|
// retentait jamais et restait vide. On distingue « en cours » de « aboutie ».
|
|
780
801
|
enCours[n]=1;
|
|
781
|
-
var gen=renderGen;
|
|
802
|
+
var gen=renderGen, monGenPage=genPage[n]||0;
|
|
782
803
|
if(IS_IMG){
|
|
783
804
|
var w=Math.round(targetWidth());
|
|
784
805
|
var im=document.createElement('img');
|
|
@@ -791,17 +812,28 @@ ${LEGAL_CSS}
|
|
|
791
812
|
return;
|
|
792
813
|
}
|
|
793
814
|
pdfDoc.getPage(n).then(function(page){
|
|
794
|
-
|
|
815
|
+
// Un build() est passe (generation globale), ou CETTE page a ete liberee entre-temps.
|
|
816
|
+
if(gen!==renderGen || monGenPage!==(genPage[n]||0)){ delete enCours[n]; return; }
|
|
795
817
|
// ATTENTION : LE DPR N ETAIT PAS PLAFONNE. Sur un ecran ×3, une page de 900 px de large fait
|
|
796
818
|
// pres de 10 M pixels, soit environ 39 Mo de tampon pour UNE page — le gain visuel au-dela de
|
|
797
819
|
// ×2 est nul, le cout est quadratique. On plafonne le DPR, puis on plafonne le NOMBRE DE
|
|
798
820
|
// PIXELS du canvas : deux bornes, parce que le zoom peut faire grandir la page independamment
|
|
799
821
|
// de la densite de l ecran.
|
|
800
|
-
var dpr=Math.min(DPR_MAX, window.devicePixelRatio||1);
|
|
801
822
|
var scale=Math.min(5,targetWidth()/page.getViewport({scale:1}).width);
|
|
802
823
|
var v=page.getViewport({scale:scale});
|
|
803
|
-
|
|
804
|
-
|
|
824
|
+
// ATTENTION : LE PLANCHER A 1 CREVAIT LE BUDGET, ET LE BUDGET ETAIT ANNONCE.
|
|
825
|
+
//
|
|
826
|
+
// L ancienne formule reduisait le DPR sans jamais le laisser descendre SOUS 1. Or au zoom
|
|
827
|
+
// maximal la taille CSS a elle seule depasse le budget : a 300 %, une page de 4110×5319 CSS
|
|
828
|
+
// faisait 21,9 M pixels — soit ×5,5 le plafond annonce, environ 83 Mo de tampon pour UNE
|
|
829
|
+
// page. Le plancher visait la nettete ; il annulait la borne exactement quand elle servait.
|
|
830
|
+
//
|
|
831
|
+
// Le facteur se calcule donc depuis la taille CSS, borne par trois choses a la fois : le
|
|
832
|
+
// plafond de densite, la densite REELLE de l ecran (ne jamais sur-rendre), et le budget de
|
|
833
|
+
// pixels — qui peut le faire descendre sous 1. Verifie par calcul sur cinq configurations
|
|
834
|
+
// avant d etre ecrit. (Releve par un audit externe.)
|
|
835
|
+
var pixelsCss=Math.max(1, v.width*v.height);
|
|
836
|
+
var dpr=Math.min(DPR_MAX, window.devicePixelRatio||1, Math.sqrt(PIXELS_MAX/pixelsCss));
|
|
805
837
|
var c=document.createElement('canvas');
|
|
806
838
|
c.setAttribute('role','img'); c.setAttribute('aria-label','Page '+n);
|
|
807
839
|
c.width=Math.floor(v.width*dpr); c.height=Math.floor(v.height*dpr); // backing store HD → net comme du natif
|
|
@@ -811,7 +843,7 @@ ${LEGAL_CSS}
|
|
|
811
843
|
taches[n]=tache;
|
|
812
844
|
tache.promise.then(function(){
|
|
813
845
|
if(taches[n]===tache) delete taches[n];
|
|
814
|
-
if(gen!==renderGen){ delete enCours[n]; return; }
|
|
846
|
+
if(gen!==renderGen || monGenPage!==(genPage[n]||0)){ delete enCours[n]; return; }
|
|
815
847
|
delete enCours[n]; rendered[n]=1; // AVEREE, pas seulement demandee
|
|
816
848
|
hideLoader(); if(n===cur)setTimeout(vsplitTint,60);
|
|
817
849
|
},function(){
|
|
@@ -822,6 +854,10 @@ ${LEGAL_CSS}
|
|
|
822
854
|
// Couche texte (sélection). En pdf.js v3, les spans utilisent font-size:calc(var(--scale-factor)*Npx)
|
|
823
855
|
// → SANS --scale-factor, taille nulle = pas de sélection. On le pose sur le conteneur.
|
|
824
856
|
try{ page.getTextContent().then(function(tc){
|
|
857
|
+
// ATTENTION : LA COUCHE TEXTE ARRIVE APRES, ET SE POSAIT SUR UNE PAGE LIBEREE. Elle
|
|
858
|
+
// ressuscitait alors une page remise en reserve — visible comme un bloc de texte
|
|
859
|
+
// selectionnable flottant sur un cadre vide.
|
|
860
|
+
if(gen!==renderGen || monGenPage!==(genPage[n]||0)) return;
|
|
825
861
|
var tl=document.createElement('div'); tl.className='textLayer';
|
|
826
862
|
tl.style.width=v.width+'px'; tl.style.height=v.height+'px'; tl.style.setProperty('--scale-factor', scale);
|
|
827
863
|
el.appendChild(tl);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CE QU'UNE APPLICATION HÔTE FOURNIT AU PLAYER.
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ Le cœur ne sait rien de l'application qui l'héberge : tout ce qu'il emprunte — stockage,
|
|
5
|
+
* base, identité, limites, marque, journalisation — arrive par cet objet, injecté une fois via
|
|
6
|
+
* `init(context)`. C'est la seule frontière, et ces types la DÉCRIVENT ; ils ne la déplacent pas.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ LA RÉFÉRENCE RESTE `docs/HOST-CONTRACT.md`. Ce fichier est une aide de frappe, pas le
|
|
9
|
+
* contrat : quand les deux divergent, le contrat gagne — et c'est ce fichier qu'il faut corriger.
|
|
10
|
+
* Les champs marqués optionnels le sont RÉELLEMENT : leur absence ferme une fonction en le
|
|
11
|
+
* disant, elle ne casse pas l'instance.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Une requête HTTP entrante, telle que le player en a besoin. Volontairement minimale :
|
|
15
|
+
* le player lit `req.query` quand la plateforme le fournit, et retombe sur `req.url` sinon —
|
|
16
|
+
* un `http.createServer` nu marche donc sans adaptateur. */
|
|
17
|
+
export interface RequeteEntrante {
|
|
18
|
+
url?: string;
|
|
19
|
+
method?: string;
|
|
20
|
+
headers: Record<string, string | string[] | undefined>;
|
|
21
|
+
query?: Record<string, unknown>;
|
|
22
|
+
body?: unknown;
|
|
23
|
+
socket?: { remoteAddress?: string };
|
|
24
|
+
on?(evenement: string, ecouteur: (...args: never[]) => void): unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** La réponse, côté Node. Le player écrit le statut, les en-têtes, puis le corps. */
|
|
28
|
+
export interface ReponseSortante {
|
|
29
|
+
statusCode: number;
|
|
30
|
+
setHeader(nom: string, valeur: string | number | readonly string[]): unknown;
|
|
31
|
+
end(corps?: unknown): unknown;
|
|
32
|
+
write?(morceau: unknown): unknown;
|
|
33
|
+
writableEnded?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Plage { start: number; end?: number }
|
|
37
|
+
|
|
38
|
+
/** Un flux lisible, décrit STRUCTURELLEMENT : ce paquet ne dépend pas de `@types/node`, et
|
|
39
|
+
* exiger cette dépendance d'un consommateur pour lire un type serait une taxe déguisée. */
|
|
40
|
+
export interface FluxLisible {
|
|
41
|
+
pipe?(destination: unknown): unknown;
|
|
42
|
+
on?(evenement: string, ecouteur: (...args: never[]) => void): unknown;
|
|
43
|
+
[Symbol.asyncIterator]?(): AsyncIterator<unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FichierRelaye {
|
|
47
|
+
body: FluxLisible | Uint8Array | null;
|
|
48
|
+
status?: number;
|
|
49
|
+
headers?: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface Stockage {
|
|
53
|
+
/** Refus par défaut : ce qui n'est pas explicitement permis ne se relaie pas. */
|
|
54
|
+
isAllowedUrl(url: string): boolean;
|
|
55
|
+
fetchFile(url: string, options?: { range?: Plage }): Promise<FichierRelaye | null>;
|
|
56
|
+
put(seau: string, chemin: string, contenu: Uint8Array, type: string): Promise<boolean>;
|
|
57
|
+
/** Optionnel : sans lui, les pièces jointes du chat sont refusées — et le player le dit.
|
|
58
|
+
* Le cœur ne doit jamais détenir la clé qui signe. */
|
|
59
|
+
signUpload?(seau: string, chemin: string): Promise<{ token: string; publicUrl: string } | null>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface BaseDeDonnees {
|
|
63
|
+
/** Forme PostgREST : `table?colonne=eq.valeur`. Volontairement sans jointure imbriquée ni
|
|
64
|
+
* arbre booléen — c'est ce qui garde un portage à la traduction plutôt qu'à la réécriture. */
|
|
65
|
+
request(chemin: string, options?: { method?: string; body?: unknown; headers?: Record<string, string> }): Promise<unknown>;
|
|
66
|
+
selectAll(chemin: string): Promise<unknown[]>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface Identite {
|
|
70
|
+
verifyToken(entete: string | undefined): Promise<unknown | null>;
|
|
71
|
+
roleOf(utilisateur: unknown): Promise<string | null> | string | null;
|
|
72
|
+
isAdmin(utilisateur: unknown): Promise<boolean> | boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Le player vérifie le jeton ; VOUS décidez des droits. Pas de réponse, ou une règle qui
|
|
75
|
+
* échoue, valent refus. L'action est passée parce que les hôtes séparent l'envoi ordinaire
|
|
76
|
+
* de l'administration.
|
|
77
|
+
*/
|
|
78
|
+
canManageShares(utilisateur: unknown, action: string): Promise<boolean> | boolean;
|
|
79
|
+
/** Optionnel : autorise VOTRE serveur à créer un lien en son nom propre. Absent ⇒ ce chemin
|
|
80
|
+
* n'existe pas. Le cœur ne voit jamais le secret : il demande, vous répondez. */
|
|
81
|
+
isTrustedHostCall?(entetes: Record<string, string | string[] | undefined>): Promise<boolean> | boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface MarqueResolue { logo: string; name: string; dark?: boolean }
|
|
85
|
+
|
|
86
|
+
export interface Marque {
|
|
87
|
+
name: string;
|
|
88
|
+
poweredBy: string;
|
|
89
|
+
loaderName: string;
|
|
90
|
+
logo(): Promise<string> | string;
|
|
91
|
+
/** `name` est le repli quand le logo ne charge pas — le champ le plus oublié, et le seul
|
|
92
|
+
* qui aide quand tout le reste échoue. */
|
|
93
|
+
forKey(cle: string): Promise<MarqueResolue | null>;
|
|
94
|
+
title(base: string, qualificatif?: string): string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface Limites {
|
|
98
|
+
/** ⚠️ Fail-open : un limiteur en panne ne doit pas tuer un lecteur. */
|
|
99
|
+
allow(cle: string, max: number, fenetreSecondes: number): Promise<boolean>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface Courriel {
|
|
103
|
+
to: string;
|
|
104
|
+
subject: string;
|
|
105
|
+
html?: string;
|
|
106
|
+
text?: string;
|
|
107
|
+
[autre: string]: unknown;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface Mentions {
|
|
111
|
+
sourceUrl: string;
|
|
112
|
+
legalUrl: string;
|
|
113
|
+
privacyUrl: string;
|
|
114
|
+
trackingNotice: string;
|
|
115
|
+
/** Optionnel : la mention d'un lien que PERSONNE n'a envoyé. Absente ⇒ repli sur la première,
|
|
116
|
+
* plutôt que de n'en afficher aucune. */
|
|
117
|
+
trackingNoticeAnonymous?: string;
|
|
118
|
+
/** ⚠️ Vide ⇒ AUCUN courriel ne part : la route répond `sendRefused: "public-url-unconfigured"`
|
|
119
|
+
* et journalise. L'en-tête `Host` ne convient pas — le client le choisit. */
|
|
120
|
+
publicUrl?: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface Reglages {
|
|
124
|
+
/** Consommé tel quel, SANS barre finale : le cœur ne renormalise plus. */
|
|
125
|
+
supabaseUrl: string;
|
|
126
|
+
supabasePublishableKey: string;
|
|
127
|
+
mapsKey: string;
|
|
128
|
+
extraFrameAncestors: string[];
|
|
129
|
+
/** Ce qui est POSÉ, à côté de ce que le code SAIT faire : la carte d'identité publie les deux,
|
|
130
|
+
* parce qu'une capacité disponible mais non configurée se comporte comme une absence. */
|
|
131
|
+
separateIssuer?: boolean;
|
|
132
|
+
hostShare?: boolean;
|
|
133
|
+
hostMail?: boolean;
|
|
134
|
+
retentionSweep?: boolean;
|
|
135
|
+
hostAuthStorageKey?: string;
|
|
136
|
+
[autre: string]: unknown;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface ContextePlayer {
|
|
140
|
+
storage: Stockage;
|
|
141
|
+
db: BaseDeDonnees;
|
|
142
|
+
identity: Identite;
|
|
143
|
+
branding: Marque;
|
|
144
|
+
limits: Limites;
|
|
145
|
+
legal: Mentions;
|
|
146
|
+
config: Reglages;
|
|
147
|
+
errors: { capture(erreur: unknown, meta?: Record<string, unknown>): unknown };
|
|
148
|
+
mail: { send(message: Courriel): Promise<{ sent: true } | null> };
|
|
149
|
+
/** Greffons appartenant à l'hôte. Le cœur affiche, trace et présente sans aucun — c'est testé. */
|
|
150
|
+
plugins: Record<string, unknown>;
|
|
151
|
+
/** Dit si un greffon est présent, sans le charger. */
|
|
152
|
+
has(nom: string): boolean;
|
|
153
|
+
schema?: unknown;
|
|
154
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `discovery-media-player` — le point d'entrée.
|
|
3
|
+
*
|
|
4
|
+
* Le player est un GESTIONNAIRE DE REQUÊTES, pas un cadriciel : vous le montez où vous voulez,
|
|
5
|
+
* les chemins sont les vôtres. Tout ce qu'il emprunte arrive par `init(context)`.
|
|
6
|
+
*
|
|
7
|
+
* const player = require("discovery-media-player");
|
|
8
|
+
* const { createStandaloneContext } = require("discovery-media-player/context/standalone");
|
|
9
|
+
* player.init(createStandaloneContext(process.env));
|
|
10
|
+
* app.use("/api/doc", (req, res) => player.handler(req, res));
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ContextePlayer, RequeteEntrante, ReponseSortante } from "./context.js";
|
|
14
|
+
|
|
15
|
+
export type { ContextePlayer, RequeteEntrante, ReponseSortante };
|
|
16
|
+
export * from "./context.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Injecte le contexte. À appeler UNE FOIS, avant la première requête.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ Le cœur garde ce contexte dans un état de module : deux instances chargées dans le même
|
|
22
|
+
* processus partagent donc ce qu'on leur injecte. Une fabrique multi-instances est un chantier
|
|
23
|
+
* ouvert ; jusque-là, une instance par processus.
|
|
24
|
+
*/
|
|
25
|
+
export function init(contexte: ContextePlayer): void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sert une requête. Lit `req.query` quand la plateforme le fournit (serverless, Express) et
|
|
29
|
+
* retombe sur `req.url` sinon — un `http.createServer` nu marche sans adaptateur.
|
|
30
|
+
*/
|
|
31
|
+
export function handler(requete: RequeteEntrante, reponse: ReponseSortante): Promise<void>;
|
|
32
|
+
|
|
33
|
+
/** Un script tiers épinglé : version exacte dans l'URL, empreinte SRI à côté. */
|
|
34
|
+
export interface Tiers { url: string; sri: string }
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* ⚠️ EXPORTÉ POUR ÊTRE CONFRONTÉ, PAS POUR ÊTRE UTILISÉ. Le banc navigateur et la garde de forge
|
|
38
|
+
* partent de cet inventaire pour vérifier qu'aucune URL de script du gabarit ne lui échappe.
|
|
39
|
+
* Un hôte n'a rien à en faire — s'il en dépend, c'est le signe d'un manque ailleurs.
|
|
40
|
+
*/
|
|
41
|
+
export const TIERS: Record<string, Tiers>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `discovery-media-player/context/standalone` — un contexte complet, construit depuis
|
|
3
|
+
* l'environnement.
|
|
4
|
+
*
|
|
5
|
+
* C'est le chemin le plus court vers une instance qui marche : un dossier de documents, et rien
|
|
6
|
+
* d'autre. Chaque réglage est une variable d'environnement, décrite dans `docs/CONFIGURATION.md`
|
|
7
|
+
* — il n'y a pas de fichier de configuration, exprès : une instance est décrite entièrement par
|
|
8
|
+
* son environnement.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ContextePlayer, BaseDeDonnees, Limites } from "./context.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Construit le contexte. `env` vaut `process.env` par défaut ; le passer explicitement permet
|
|
15
|
+
* de servir deux marques depuis un même processus, ou d'éprouver une configuration sans la poser.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ Ce qui n'est pas configuré est FERMÉ, pas deviné : sans base, les liens tracés n'existent
|
|
18
|
+
* pas ; sans `PLAYER_PUBLIC_URL`, aucun courriel ne part. Un refus nommé vaut mieux qu'un repli
|
|
19
|
+
* silencieux.
|
|
20
|
+
*/
|
|
21
|
+
export function createStandaloneContext(env?: Record<string, string | undefined>): ContextePlayer;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Le limiteur de débit du contexte autonome, exposé pour qui compose son propre contexte.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ Fail-open, délibérément : un limiteur en panne ne doit pas empêcher un lecteur d'ouvrir son
|
|
27
|
+
* document. Il protège d'un abus, il ne garde pas une porte.
|
|
28
|
+
*/
|
|
29
|
+
export function creerLimites(db: BaseDeDonnees, journal: { capture(erreur: unknown, meta?: Record<string, unknown>): unknown }): Limites;
|