discovery-media-player 0.1.3 → 0.1.5
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/CONTRAT.md +82 -6
- package/README.md +30 -4
- package/package.json +1 -1
- package/server/__tests__/plateforme.test.js +82 -0
- package/server/handler.js +52 -4
package/CONTRAT.md
CHANGED
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
## Les cinq règles
|
|
16
16
|
|
|
17
|
-
1. **Un seul dépôt de vérité.** Le player se corrige dans le
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
une version périmée sans que
|
|
17
|
+
1. **Un seul dépôt de vérité.** Le player se corrige **dans le dépôt du player**, jamais dans un
|
|
18
|
+
hôte — pas même dans celui qui l'a écrit à l'origine, qui l'installe aujourd'hui depuis npm
|
|
19
|
+
comme tous les autres. Un correctif écrit côté hôte est une copie qui divergera : le précédent
|
|
20
|
+
est documenté — un runtime copié dans 4 dépôts, 3 sur 4 servaient une version périmée sans que
|
|
21
|
+
personne ne le voie.
|
|
21
22
|
2. **Additif par défaut.** Ajouter une action, un paramètre ou un champ ne casse aucun hôte : c'est
|
|
22
23
|
libre, ça se note au journal. **Retirer ou renommer est une rupture** → nouvelle version, les
|
|
23
24
|
deux servies pendant la migration.
|
|
@@ -43,7 +44,33 @@
|
|
|
43
44
|
**Qui prévient qui.** Une PR d'hôte qui exige une version plus récente l'écrit **dans son titre**
|
|
44
45
|
(« requiert player ≥ v2 »). Elle ne peut pas être mergée avant que l'instance correspondante soit
|
|
45
46
|
déployée. C'est la seule règle nécessaire : il n'y a qu'une personne qui déploie les deux.
|
|
46
|
-
5. **
|
|
47
|
+
5. **Qui corrige le module générique.** Cette règle disait « un besoin d'hôte se demande, il ne
|
|
48
|
+
se code pas sur place ». Elle datait d'avant la publication, quand le player vivait dans un
|
|
49
|
+
hôte et qu'aucun autre ne pouvait y toucher. Maintenant qu'il a son dépôt, elle est trop
|
|
50
|
+
étroite : un hôte **peut** coder — dans le bon dépôt.
|
|
51
|
+
|
|
52
|
+
**N'importe quel hôte propose. Le mainteneur arbitre et publie.**
|
|
53
|
+
|
|
54
|
+
| | |
|
|
55
|
+
|---|---|
|
|
56
|
+
| Un hôte trouve un défaut du player | il ouvre une **issue ou une PR sur le dépôt du player** — il a le contexte, souvent déjà le code |
|
|
57
|
+
| Le mainteneur | tranche, fusionne, **publie la version** |
|
|
58
|
+
| Les hôtes | épinglent la nouvelle version quand ils décident de la prendre |
|
|
59
|
+
|
|
60
|
+
⚠️ **La publication reste au mainteneur, et ce n'est pas une question de hiérarchie.** Publier
|
|
61
|
+
une version décide de l'ordre de déploiement (règle 3). Si chacun publie, plus personne ne sait
|
|
62
|
+
quelle instance tourne sur quoi.
|
|
63
|
+
|
|
64
|
+
⚠️ **Et l'arbitrage n'est pas une formalité : un hôte optimise pour son cas, c'est normal.**
|
|
65
|
+
Exemple vécu — un hôte a corrigé chez lui, en trois lignes, le fait que le gestionnaire lise
|
|
66
|
+
`req.query` sur une plateforme qui ne le remplit pas. Son correctif était juste. Le bon
|
|
67
|
+
correctif était **dans le cœur**, parce que le défaut touchait tous les hôtes, présents et à
|
|
68
|
+
venir. Seul quelqu'un qui tient les contraintes des deux côtés voit ça.
|
|
69
|
+
|
|
70
|
+
**Le contournement local est autorisé quand il débloque**, à deux conditions : le signalement
|
|
71
|
+
est ouvert **le jour même**, et le contournement est **retiré quand la version arrive**. Sinon
|
|
72
|
+
il devient permanent, et on a deux implémentations qui divergent — c'est-à-dire le problème que
|
|
73
|
+
ce contrat existe pour empêcher.
|
|
47
74
|
|
|
48
75
|
---
|
|
49
76
|
|
|
@@ -181,6 +208,47 @@ lire n'importe quoi, avec des identifiants : c'est précisément ce que la garde
|
|
|
181
208
|
*(Le player applique lui-même cette règle depuis le 13/08 — `relayerFichier()`, un seul chemin
|
|
182
209
|
pour ses trois routes de streaming. Le piège nous concernait aussi.)*
|
|
183
210
|
|
|
211
|
+
⚠️ **ET UNE QUATRIÈME, D'UNE AUTRE NATURE — celle-ci n'abîme pas l'expérience, elle ouvre les
|
|
212
|
+
données.**
|
|
213
|
+
|
|
214
|
+
Les trois précédentes portent sur le TRANSPORT. Celle-ci porte sur ce qu'on transporte :
|
|
215
|
+
|
|
216
|
+
> **Ce que votre route accepte de signer est ce que n'importe quel appelant peut lire.
|
|
217
|
+
> Ne signez jamais un chemin fourni par le client.**
|
|
218
|
+
|
|
219
|
+
Le raisonnement tient en trois phrases. Le player va chercher le fichier **serveur à serveur** —
|
|
220
|
+
il n'a, par construction, aucune session à faire valoir : c'est tout l'objet du secret partagé.
|
|
221
|
+
Votre route le sert donc avec **ses propres droits**, souvent une clé de service qui contourne
|
|
222
|
+
vos politiques de ligne. Une action qui signe un chemin reçu du navigateur devient alors un
|
|
223
|
+
oracle : un utilisateur fait signer un chemin que ses droits lui refusent, ouvre l'aperçu, et le
|
|
224
|
+
player le lui lit avec les vôtres.
|
|
225
|
+
|
|
226
|
+
**La garde anti-SSRF ne voit rien** — l'origine est parfaitement légitime, c'est la vôtre.
|
|
227
|
+
|
|
228
|
+
La forme qui tient : **l'appelant fournit une SOURCE d'un ensemble fermé et un IDENTIFIANT de
|
|
229
|
+
ligne, jamais un chemin.** Le chemin est relu en base avec la session de l'appelant, et vos
|
|
230
|
+
politiques tranchent comme partout ailleurs. C'est la même règle que `brandKey` (une référence,
|
|
231
|
+
pas une copie) et que `PLAYER_HOST_FETCH_BASE` (un préfixe, pas une origine) : **on transmet de
|
|
232
|
+
quoi retrouver, jamais de quoi désigner.**
|
|
233
|
+
|
|
234
|
+
⚠️ **Le piège est qu'elle est souvent théorique le jour où on l'écrit.** Si vos politiques
|
|
235
|
+
laissent aujourd'hui tout membre connecté lire, l'élévation n'existe pas encore — elle apparaîtra
|
|
236
|
+
au premier resserrement, des mois plus tard, et personne ne fera le lien entre « on a restreint un
|
|
237
|
+
accès » et « une route signe encore n'importe quoi ».
|
|
238
|
+
|
|
239
|
+
**Corollaire, rencontré par le même hôte une semaine plus tard : quand ce que la référence
|
|
240
|
+
TRANSPORTE est elle-même une capacité, signer ne suffit pas — il faut chiffrer.** Une référence
|
|
241
|
+
signée reste lisible : le base64 se décode. *Signé* veut dire « personne ne peut le forger » ; ça
|
|
242
|
+
n'a jamais voulu dire « personne ne peut le lire ». Si votre référence contient une URL qui sert le
|
|
243
|
+
fichier sans authentification et que rien n'expire, la publier en clair revient à publier le
|
|
244
|
+
fichier.
|
|
245
|
+
|
|
246
|
+
*(Signalée par le second hôte après l'avoir rencontrée en basculant ses premières surfaces.
|
|
247
|
+
Vérifiée chez l'hôte historique le jour même : une route y signait un chemin reçu du client,
|
|
248
|
+
derrière une liste NOIRE de rôles — un compte d'espace client passait, et tout rôle créé plus tard
|
|
249
|
+
serait passé aussi.)*
|
|
250
|
+
|
|
251
|
+
|
|
184
252
|
### Un refus se dit — `embed-denied`
|
|
185
253
|
|
|
186
254
|
Un hôte qui intègre la visionneuse (`?embed=1`) attend `embed-ready`. Il est tentant d'en faire un
|
|
@@ -340,7 +408,13 @@ documents **sans passer par le player**, donc sans être comptée. Ce n'est pas
|
|
|
340
408
|
c'est la pente naturelle d'un produit vivant — un `<iframe src="....pdf">` s'écrit en dix secondes.
|
|
341
409
|
|
|
342
410
|
**Chaque hôte doit tenir la liste de ses portes et la rechasser périodiquement.** Une recherche
|
|
343
|
-
suffit : `.pdf`, `window.open`, `<embed`, `<iframe` sur un fichier, `application/pdf`.
|
|
411
|
+
suffit : `.pdf`, `window.open`, `<embed`, `<iframe` sur un fichier, `application/pdf`.
|
|
412
|
+
|
|
413
|
+
⚠️ **Et le critère de recherche décide de ce qu'on trouve.** Un hôte a inventorié ses portes en
|
|
414
|
+
cherchant les appels de son moteur de stockage — et a manqué son plus gros gisement de documents,
|
|
415
|
+
parce que ces fichiers-là ne sont pas dans son stockage. Aucune recherche de cette forme ne pouvait
|
|
416
|
+
les voir. Cherchez par ce que l'utilisateur OBTIENT (un document qui s'ouvre), pas par la
|
|
417
|
+
technique que vous vous attendez à trouver. Le tableau
|
|
344
418
|
des portes recensées vit chez chaque hôte, pas ici. La règle, elle, est commune : **une porte non
|
|
345
419
|
recensée est une lecture non comptée**, et l'écart ne se voit dans aucune statistique — il se voit
|
|
346
420
|
seulement quand quelqu'un le cherche.
|
|
@@ -522,6 +596,8 @@ Toute évolution de la frontière se note ici, datée, avec sa nature.
|
|
|
522
596
|
| 2026-08-13 | précision | **Le câblage d'une instance appartient à l'HÔTE** (4 fichiers, un seul à écrire) : il ne contient que des décisions de l'hôte. `forKey` d'une instance séparée **appelle une route de l'hôte** plutôt que de recopier la correspondance clé → logo. |
|
|
523
597
|
| 2026-08-13 | additif | **`GET /api/doc?contract=1`** existe enfin : la règle 4 reposait sur un point qui n'avait jamais été écrit. Carte d'identité sans session, sans base, sans cache, sans URL ni secret. |
|
|
524
598
|
| 2026-08-13 | additif | **Le schéma part avec le player** (`player/supabase/init.sql`) : un fichier rejouable qui amène une base vierge à l'état attendu, **déjà durci** — une instance neuve ne connaît jamais l'état « lecture anonyme ouverte ». |
|
|
599
|
+
| 2026-08-13 | précision | **Règle 5 réécrite : qui corrige le module générique.** Elle interdisait à un hôte de coder — c'était vrai quand le player vivait dans un hôte. N'importe quel hôte **propose** désormais (issue ou PR sur le dépôt du player) ; **le mainteneur arbitre et publie**, parce que publier décide de l'ordre de déploiement. Contournement local autorisé s'il débloque, à condition d'ouvrir le signalement le jour même et de le retirer à l'arrivée de la version. |
|
|
600
|
+
| 2026-08-13 | précision | **Règle 1 recadrée** : « le player se corrige dans le studio » devient « dans le dépôt du player » — l'hôte historique l'installe depuis npm comme les autres. |
|
|
525
601
|
| 2026-08-13 | décision | **Dépôt public dès la création, AGPL-3.0** : `PLAYER_SOURCE_URL` pointe dessus, aucun jeton ni clé de déploiement chez les hôtes. Le câblage d'un hôte est considéré couvert — il ne doit donc contenir aucun secret en clair, ce qui est de toute façon la bonne façon de l'écrire. |
|
|
526
602
|
| 2026-08-13 | décision | **Aucun nom de tiers dans le dépôt publié** : les hôtes sont désignés par leur RÔLE (« l'hôte historique », « le second hôte »), jamais par leur raison sociale, et les exemples d'URL sont fictifs. Le rôle porte toute l'information technique ; le nom ne dit qu'une chose — quelles entreprises travaillent ensemble et où leurs documents vivent. |
|
|
527
603
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Juli1artha/discovery-media-player/main/assets/logo-dark.svg">
|
|
5
|
+
<img alt="Discovery Media Player" src="https://raw.githubusercontent.com/Juli1artha/discovery-media-player/main/assets/logo.svg" width="520">
|
|
6
|
+
</picture>
|
|
7
|
+
|
|
8
|
+
<br><br>
|
|
4
9
|
|
|
5
10
|
**Send a document. Know if it was read.**
|
|
6
11
|
|
|
@@ -13,19 +18,32 @@ to a third-party SaaS.
|
|
|
13
18
|
[](package.json)
|
|
14
19
|
[](#docker)
|
|
15
20
|
|
|
21
|
+
<br>
|
|
22
|
+
|
|
23
|
+
<img src="https://raw.githubusercontent.com/Juli1artha/discovery-media-player/main/assets/captures/viewer.png" alt="The viewer: a document, a toolbar, and the tracked-reading timer running" width="900">
|
|
24
|
+
|
|
25
|
+
<br>
|
|
26
|
+
|
|
16
27
|
</div>
|
|
17
28
|
|
|
18
29
|
---
|
|
19
30
|
|
|
20
31
|
## Try it in two minutes
|
|
21
32
|
|
|
33
|
+
**[▶ Open the live demo](https://discovery-media-player-demo.vercel.app)** — a document, in the
|
|
34
|
+
real viewer, nothing to install.
|
|
35
|
+
|
|
36
|
+
Or on your own machine, over your own files:
|
|
37
|
+
|
|
22
38
|
```bash
|
|
23
39
|
docker run --rm -p 3000:3000 -v "$PWD/documents:/data" ghcr.io/juli1artha/discovery-media-player
|
|
24
40
|
```
|
|
25
41
|
|
|
26
|
-
Drop a PDF in `./documents
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
Drop a PDF in `./documents` and open `http://localhost:3000`. No database, no account, no
|
|
43
|
+
configuration — the viewer, progressive page loading and the reading timer all work from a folder
|
|
44
|
+
on disk.
|
|
45
|
+
|
|
46
|
+
<img src="https://raw.githubusercontent.com/Juli1artha/discovery-media-player/main/assets/captures/folder.png" alt="Folder mode: the server lists what it can display" width="760">
|
|
29
47
|
|
|
30
48
|
From source, the same thing:
|
|
31
49
|
|
|
@@ -87,6 +105,9 @@ forking it. A fix lands once and reaches every instance on its next deploy.
|
|
|
87
105
|
- **Node / Express / Next.js** — same handler, mounted on a route
|
|
88
106
|
- **Standalone** — `npm start`, or the Docker image
|
|
89
107
|
|
|
108
|
+
It reads `req.query` when the platform provides it (serverless, Express) and falls back to parsing
|
|
109
|
+
`req.url` when it does not — so a bare `http.createServer` works too, without a shim.
|
|
110
|
+
|
|
90
111
|
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the boundary, and
|
|
91
112
|
[`docs/API.md`](docs/API.md) for the surface an integrator implements.
|
|
92
113
|
|
|
@@ -135,6 +156,11 @@ if you run a modified version and people read documents through it over a networ
|
|
|
135
156
|
able to obtain your source. Set `PLAYER_SOURCE_URL` to where yours lives — the pages served
|
|
136
157
|
link to it.
|
|
137
158
|
|
|
159
|
+
**The name and the logo are not covered by it.** `assets/` and the words *Discovery Media
|
|
160
|
+
Player* are trademarks of 3D Discovery: fork the code freely, but call your fork something else.
|
|
161
|
+
This is the usual arrangement in open source, and it protects you as much as us — nobody should
|
|
162
|
+
be able to publish something under this name that we did not write.
|
|
163
|
+
|
|
138
164
|
One exception, on purpose: **[`src/bridge.ts`](src/bridge.ts) is MIT**
|
|
139
165
|
([`LICENSE-MIT`](LICENSE-MIT)). It is the message contract a host application imports to talk to
|
|
140
166
|
the player. Putting it under the core licence would make integration itself a toll. We protect
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// LE GESTIONNAIRE NE DOIT DÉPENDRE D'AUCUNE PLATEFORME.
|
|
2
|
+
//
|
|
3
|
+
// Le README promet « Vercel, Next.js, Express, ou le serveur HTTP de Node — le même gestionnaire ».
|
|
4
|
+
// C'était vrai à une convention près, jamais écrite : il lisait `req.query`, que les plateformes
|
|
5
|
+
// serverless et Express remplissent, mais qu'un serveur HTTP nu laisse indéfini.
|
|
6
|
+
//
|
|
7
|
+
// ⚠️ ET LE SYMPTÔME ÉTAIT LE PIRE POSSIBLE. Sans paramètres, la requête cherchait un partage nommé
|
|
8
|
+
// « rien » et rendait « Ce lien n'est plus valide ou a été révoqué ». Un intégrateur voyait un
|
|
9
|
+
// REFUS là où il lui manquait un branchement — l'inversion exacte qu'on passe notre temps à
|
|
10
|
+
// corriger. Signalé par un hôte qui montait le player sur http.createServer : chez lui, ça aurait
|
|
11
|
+
// marché en production par chance, pas par construction.
|
|
12
|
+
|
|
13
|
+
const player = require("../handler.js");
|
|
14
|
+
|
|
15
|
+
function contexteMinimal() {
|
|
16
|
+
return {
|
|
17
|
+
plugins: {}, has: () => false,
|
|
18
|
+
storage: { isAllowedUrl: () => false, async fetchFile() { return null; }, async put() {} },
|
|
19
|
+
db: { async request() { return []; }, async selectAll() { return []; } },
|
|
20
|
+
mail: { async send() {} },
|
|
21
|
+
identity: { async verifyToken() { return null; }, roleOf: () => "", isAdmin: () => false, async canManageShares() { return false; } },
|
|
22
|
+
limits: { async allow() { return true; } },
|
|
23
|
+
branding: { async logo() { return ""; }, name: "", poweredBy: "", loaderName: "", async forKey() { return null; }, title: (b) => b },
|
|
24
|
+
errors: { async capture() {} },
|
|
25
|
+
legal: { sourceUrl: "", legalUrl: "", privacyUrl: "", trackingNotice: "" },
|
|
26
|
+
config: { supabaseUrl: "", supabasePublishableKey: "", mapsKey: "", extraFrameAncestors: [] },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function appel(req) {
|
|
31
|
+
player.init(contexteMinimal());
|
|
32
|
+
const res = {
|
|
33
|
+
statusCode: 0, headers: {}, body: "",
|
|
34
|
+
setHeader(k, v) { this.headers[k.toLowerCase()] = v; },
|
|
35
|
+
end(b) { this.body = String(b == null ? "" : b); },
|
|
36
|
+
};
|
|
37
|
+
await player.handler({ method: "GET", headers: {}, socket: {}, ...req }, res);
|
|
38
|
+
return res;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("d'où viennent les paramètres", () => {
|
|
42
|
+
// La convention serverless : la plateforme a déjà analysé l'URL.
|
|
43
|
+
it("utilise req.query quand la plateforme le fournit", async () => {
|
|
44
|
+
const res = await appel({ query: { contract: "1" } });
|
|
45
|
+
expect(res.statusCode).toBe(200);
|
|
46
|
+
expect(JSON.parse(res.body).contract).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ⚠️ Le cas qui produisait « lien révoqué ». Un serveur HTTP nu ne remplit pas req.query.
|
|
50
|
+
it("les retrouve dans req.url quand elle ne le fournit pas", async () => {
|
|
51
|
+
const res = await appel({ url: "/api/doc?contract=1" });
|
|
52
|
+
expect(res.statusCode).toBe(200);
|
|
53
|
+
expect(JSON.parse(res.body).contract).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("ne se laisse pas troubler par une URL absurde", async () => {
|
|
57
|
+
const res = await appel({ url: "pas une url du tout" });
|
|
58
|
+
expect(res.statusCode).toBe(400);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("quand rien n'est demandé", () => {
|
|
63
|
+
// ⚠️ LE POINT. Ni slug, ni présentation, ni aperçu : il n'y a rien à afficher, et ce n'est PAS
|
|
64
|
+
// un refus. Rendre la page de révocation envoyait l'intégrateur chercher un lien mort.
|
|
65
|
+
it("le dit, au lieu d'afficher « lien révoqué »", async () => {
|
|
66
|
+
const res = await appel({ query: {} });
|
|
67
|
+
expect(res.statusCode).toBe(400);
|
|
68
|
+
expect(res.body).toContain("Aucun document demandé");
|
|
69
|
+
expect(res.body).not.toContain("révoqué");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("oriente vers la cause réelle : les paramètres de requête", async () => {
|
|
73
|
+
expect((await appel({ query: {} })).body).toMatch(/paramètres de requête/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Un slug inconnu, LUI, est bien un refus : la distinction doit rester nette dans les deux sens.
|
|
77
|
+
it("un slug inconnu reste un refus, pas une erreur de branchement", async () => {
|
|
78
|
+
const res = await appel({ query: { slug: "Inconnu-1234" } });
|
|
79
|
+
expect(res.statusCode).toBe(404);
|
|
80
|
+
expect(res.body).toContain("révoqué");
|
|
81
|
+
});
|
|
82
|
+
});
|
package/server/handler.js
CHANGED
|
@@ -2113,9 +2113,34 @@ async function readJsonBody(req) {
|
|
|
2113
2113
|
});
|
|
2114
2114
|
}
|
|
2115
2115
|
|
|
2116
|
+
/**
|
|
2117
|
+
* Paramètres de la requête, quelle que soit la plateforme.
|
|
2118
|
+
*
|
|
2119
|
+
* Le gestionnaire lit `req.query` — la convention des plateformes serverless (Vercel, Next.js) et
|
|
2120
|
+
* d'Express. Un serveur HTTP nu ne la remplit pas : `req.query` est alors `undefined`, et TOUT
|
|
2121
|
+
* paramètre disparaît.
|
|
2122
|
+
*
|
|
2123
|
+
* ⚠️ CE QUE ÇA DONNAIT, ET POURQUOI C'ÉTAIT LE PIRE DES SYMPTÔMES. Sans paramètres, la requête
|
|
2124
|
+
* partait chercher un partage nommé « rien », n'en trouvait pas, et affichait « Ce lien n'est plus
|
|
2125
|
+
* valide ou a été révoqué ». Un intégrateur voyait donc un REFUS là où il n'avait simplement pas
|
|
2126
|
+
* branché la plateforme. C'est exactement l'inversion qu'on passe notre temps à corriger : une
|
|
2127
|
+
* erreur de câblage ne doit jamais ressembler à une décision.
|
|
2128
|
+
*
|
|
2129
|
+
* Signalé par un hôte qui montait le player sur `http.createServer` : chez lui ça aurait marché en
|
|
2130
|
+
* production (Vercel remplit `req.query`) — par chance, pas par construction.
|
|
2131
|
+
*/
|
|
2132
|
+
function parametres(req) {
|
|
2133
|
+
if (req.query && typeof req.query === "object") return req.query;
|
|
2134
|
+
try {
|
|
2135
|
+
return Object.fromEntries(new URL(req.url || "/", "http://interne").searchParams);
|
|
2136
|
+
} catch {
|
|
2137
|
+
return {};
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2116
2141
|
async function handler(req, res) {
|
|
2117
2142
|
try {
|
|
2118
|
-
const q = req
|
|
2143
|
+
const q = parametres(req);
|
|
2119
2144
|
const slug = String(q.slug || "").trim();
|
|
2120
2145
|
|
|
2121
2146
|
if (req.method === "POST") {
|
|
@@ -2496,6 +2521,17 @@ async function handler(req, res) {
|
|
|
2496
2521
|
return;
|
|
2497
2522
|
}
|
|
2498
2523
|
|
|
2524
|
+
// AUCUN DOCUMENT DEMANDÉ. Ni slug, ni présentation, ni aperçu, ni carte d'identité — il n'y a
|
|
2525
|
+
// rien à afficher, et ce n'est pas un refus. Le dire franchement évite qu'un intégrateur
|
|
2526
|
+
// cherche un lien révoqué là où il lui manque un paramètre.
|
|
2527
|
+
if (req.method === "GET" && !slug && !q.present && !q.preview && !q.contract) {
|
|
2528
|
+
res.statusCode = 400;
|
|
2529
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
2530
|
+
res.end("Aucun document demandé. Attendu : ?slug=… , ?present=… , ?preview=1 ou ?contract=1.\n" +
|
|
2531
|
+
"Si vous intégrez le player, vérifiez que la plateforme fournit les paramètres de requête.");
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2499
2535
|
// ── CARTE D'IDENTITÉ (`?contract=1`) ─────────────────────────────────────────────────────
|
|
2500
2536
|
// La règle 4 du contrat demande à l'hôte d'épingler la version qu'il vise et de le VÉRIFIER.
|
|
2501
2537
|
// Sans point d'interrogation, cette règle était une intention : un hôte ne pouvait pas écrire
|
|
@@ -2678,10 +2714,22 @@ async function handler(req, res) {
|
|
|
2678
2714
|
// (+ extras via DOC_FRAME_ANCESTORS, séparés par des espaces — futurs domaines
|
|
2679
2715
|
// custom d'XP). La CSP frame-ancestors PRIME sur le X-Frame-Options SAMEORIGIN
|
|
2680
2716
|
// global du vercel.json (spec : XFO ignoré quand frame-ancestors est présent).
|
|
2717
|
+
// ⚠️ EMBARQUEMENT DEMANDÉ SANS HÔTE AUTORISÉ : le seul cas où le player ne peut pas se
|
|
2718
|
+
// défendre lui-même. C'est le NAVIGATEUR qui bloque, avant que la page ne soit chargée —
|
|
2719
|
+
// donc aucun `embed-denied` ne peut partir, et l'hôte voit un silence indiscernable d'une
|
|
2720
|
+
// instance injoignable. Le signaler ici est la seule occasion : c'est le moment exact où l'on
|
|
2721
|
+
// sait qu'on est destiné à être encadré. Sans DOC_FRAME_ANCESTORS, personne ne peut AFFICHER,
|
|
2722
|
+
// exactement comme sans PLAYER_HOST_AUTHZ_URL personne ne peut DIFFUSER.
|
|
2723
|
+
if (share.embed && !(PLAYER.config.extraFrameAncestors || []).length) {
|
|
2724
|
+
try {
|
|
2725
|
+
PLAYER.errors.capture(
|
|
2726
|
+
new Error("?embed=1 demandé mais DOC_FRAME_ANCESTORS est vide : seuls une page de même origine et *.vercel.app peuvent encadrer cette instance"),
|
|
2727
|
+
{ route: "doc", indice: "le navigateur bloquera l'iframe avant le chargement — aucun embed-denied ne partira" },
|
|
2728
|
+
);
|
|
2729
|
+
} catch { /* jamais bloquant */ }
|
|
2730
|
+
}
|
|
2681
2731
|
const frameAncestors = share.embed
|
|
2682
|
-
?
|
|
2683
|
-
.concat(String(process.env.DOC_FRAME_ANCESTORS || "").split(/\s+/).filter(Boolean))
|
|
2684
|
-
.join(" ")
|
|
2732
|
+
? embedFrameAncestors()
|
|
2685
2733
|
: "'self'";
|
|
2686
2734
|
return sendHtml(res, 200, viewerHtml(share, nonce, logoUrl, pitch), `'nonce-${nonce}'`, [originOf(logoUrl), originOf(share.bot_avatar), originOf(share.brand_logo)].filter(Boolean).join(" "), frameAncestors);
|
|
2687
2735
|
} catch (error) {
|