discovery-media-player 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -87,6 +87,9 @@ forking it. A fix lands once and reaches every instance on its next deploy.
87
87
  - **Node / Express / Next.js** — same handler, mounted on a route
88
88
  - **Standalone** — `npm start`, or the Docker image
89
89
 
90
+ It reads `req.query` when the platform provides it (serverless, Express) and falls back to parsing
91
+ `req.url` when it does not — so a bare `http.createServer` works too, without a shim.
92
+
90
93
  See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the boundary, and
91
94
  [`docs/API.md`](docs/API.md) for the surface an integrator implements.
92
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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.query || {};
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