discovery-media-player 0.1.124 → 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.124",
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"
@@ -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;