discovery-media-player 0.1.123 → 0.1.125

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.
@@ -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. Vide ⇒ repli sur `Host`, mais signalé.
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();
@@ -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.123",
3
+ "version": "0.1.125",
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
- ".": "./server/handler.js",
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": "./context/standalone.js",
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"
@@ -321,7 +321,84 @@ var Live=(function(){
321
321
  function renderMsgInner(m){return Player.chat.renderMessage(m,{me:ME,reactIcon:RSVG});}
322
322
  function cmClass(m){return Player.chat.messageClassName(m,ME,isMentioned(m));}
323
323
  function hydratePdf(d,m){if(m&&m.attachment&&m.attachment.kind==='pdf'&&!m.deleted){var ph=d.querySelector('.cm-att-ph');if(ph)pdfThumb(m.attachment.url,ph);}}
324
- function pdfThumb(url,ph){if(!ph)return;if(pdfCache[url]){ph.innerHTML='<img src="'+pdfCache[url]+'" alt="">';return;}if(!window.pdfjsLib)return;try{pdfjsLib.getDocument({url:url,isEvalSupported:false}).promise.then(function(pdf){return pdf.getPage(1);}).then(function(pg){var v0=pg.getViewport({scale:1}),sc=Math.min(1.6,208/v0.width),vp=pg.getViewport({scale:sc}),cv=document.createElement('canvas');cv.width=Math.ceil(vp.width);cv.height=Math.ceil(vp.height);return pg.render({canvasContext:cv.getContext('2d'),viewport:vp}).promise.then(function(){var u=cv.toDataURL('image/jpeg',0.8);pdfCache[url]=u;ph.innerHTML='<img src="'+u+'" alt="">';});}).catch(function(){});}catch(e){}}
324
+ // LES VIGNETTES DU CHAT NE SONT PLUS SANS BORNE (audit externe, perf navigateur).
325
+ //
326
+ // L ancienne version tenait en une ligne et cumulait quatre defauts : le cache etait SANS LIMITE
327
+ // et gardait des data-URL (du base64 en memoire, environ 1,33 fois le binaire) ; deux messages
328
+ // portant le meme PDF lancaient DEUX chargements ; le PDFDocumentProxy n etait jamais detruit,
329
+ // donc son worker gardait le document entier pour une vignette de 208 px ; et tout etait charge
330
+ // AU RENDU, pas a l affichage — un fil de deux cents pieces jointes chargeait deux cents PDF.
331
+ //
332
+ // Note de forme : ce fichier est un litteral de gabarit — pas d accent grave ici.
333
+ var VIGN_MAX=24, VIGN_PARALLELE=2;
334
+ var vignUrl=Object.create(null), vignOrdre=[], vignEnVol=Object.create(null), vignFile=[], vignActifs=0, vignIO=null;
335
+ // ATTENTION : REVOQUER UNE URL ENCORE AFFICHEE VIDE L IMAGE. L eviction ne peut pas se contenter
336
+ // de liberer : elle doit REMETTRE en reserve les vignettes qui utilisaient l URL revoquee, et les
337
+ // re-observer, sinon le lecteur voit des cases blanches en remontant le fil.
338
+ function vignRendre(ph){ if(!ph)return; ph.innerHTML=''; if(vignIO&&ph.dataset&&ph.dataset.pdfUrl)vignIO.observe(ph); }
339
+ function vignEvincer(){
340
+ while(vignOrdre.length>VIGN_MAX){
341
+ var vieux=vignOrdre.shift(), u=vignUrl[vieux];
342
+ delete vignUrl[vieux];
343
+ if(!u)continue;
344
+ try{
345
+ var imgs=document.querySelectorAll('.cm-att-ph img');
346
+ for(var i=0;i<imgs.length;i++){ if(imgs[i].src===u) vignRendre(imgs[i].parentNode); }
347
+ }catch(e){}
348
+ try{ URL.revokeObjectURL(u); }catch(e){}
349
+ }
350
+ }
351
+ function vignPoser(url,u){ if(!u)return; vignUrl[url]=u; vignOrdre.push(url); vignEvincer(); }
352
+ function vignAfficher(ph,u){ if(ph&&u) ph.innerHTML='<img src="'+u+'" alt="">'; }
353
+ // File de concurrence : deux vignettes a la fois. Sans elle, ouvrir un fil charge autant de PDF
354
+ // que de pieces jointes visibles d un coup — la vignette est un confort, pas une priorite.
355
+ function vignSuivante(){
356
+ if(vignActifs>=VIGN_PARALLELE||!vignFile.length)return;
357
+ var t=vignFile.shift(); vignActifs++;
358
+ t().catch(function(){}).then(function(){ vignActifs--; vignSuivante(); });
359
+ }
360
+ function vignCharger(url){
361
+ if(vignEnVol[url])return vignEnVol[url]; // meme URL deja en vol : on partage
362
+ var p=new Promise(function(resoudre,rejeter){
363
+ vignFile.push(function(){
364
+ if(!window.pdfjsLib)return Promise.reject(new Error('pdf.js absent'));
365
+ return pdfjsLib.getDocument({url:url,isEvalSupported:false}).promise.then(function(pdf){
366
+ return pdf.getPage(1).then(function(pg){
367
+ var v0=pg.getViewport({scale:1}), sc=Math.min(1.6,208/v0.width), vp=pg.getViewport({scale:sc});
368
+ var cv=document.createElement('canvas'); cv.width=Math.ceil(vp.width); cv.height=Math.ceil(vp.height);
369
+ return pg.render({canvasContext:cv.getContext('2d'),viewport:vp}).promise.then(function(){
370
+ // toBlob plutot que toDataURL : une URL objet pointe le binaire, une data-URL en garde
371
+ // une copie base64 dans le tas — et rien ne la libere jamais.
372
+ return new Promise(function(ok){ cv.toBlob(function(b){ ok(b?URL.createObjectURL(b):''); },'image/jpeg',0.8); });
373
+ });
374
+ }).then(function(u){ try{ pdf.destroy(); }catch(e){} return u; },
375
+ function(e){ try{ pdf.destroy(); }catch(e2){} throw e; });
376
+ }).then(function(u){ vignPoser(url,u); resoudre(u); },function(e){ rejeter(e); });
377
+ });
378
+ vignSuivante();
379
+ });
380
+ vignEnVol[url]=p;
381
+ p.catch(function(){}).then(function(){ delete vignEnVol[url]; });
382
+ return p;
383
+ }
384
+ function pdfThumb(url,ph){
385
+ if(!ph||!url)return;
386
+ if(vignUrl[url]){ vignAfficher(ph,vignUrl[url]); return; }
387
+ try{ ph.dataset.pdfUrl=url; }catch(e){}
388
+ // Chargement PARESSEUX : on ne fabrique la vignette que lorsque la piece jointe approche du
389
+ // viewport. Un fil ancien de deux cents pieces jointes ne charge plus deux cents documents.
390
+ if(typeof IntersectionObserver!=='function'){ vignCharger(url).then(function(u){ vignAfficher(ph,u); },function(){}); return; }
391
+ if(!vignIO) vignIO=new IntersectionObserver(function(es){
392
+ for(var i=0;i<es.length;i++){
393
+ if(!es[i].isIntersecting)continue;
394
+ var el=es[i].target, u2=(el.dataset&&el.dataset.pdfUrl)||''; vignIO.unobserve(el);
395
+ if(!u2)continue;
396
+ if(vignUrl[u2]){ vignAfficher(el,vignUrl[u2]); continue; }
397
+ (function(cible){ vignCharger(u2).then(function(u){ vignAfficher(cible,u); },function(){}); })(el);
398
+ }
399
+ },{rootMargin:'200px'});
400
+ vignIO.observe(ph);
401
+ }
325
402
  // Renvoie true seulement si le message a réellement été AJOUTÉ. Pendant la transition, il
326
403
  // arrive par deux voies (diffusion et lecture de table) : sans cette réponse, l'affichage était
327
404
  // bien dédoublonné mais le compteur de non-lus comptait deux fois — une pastille à 2 pour un
@@ -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
+ }
@@ -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;