discovery-media-player 0.1.170 → 0.1.172
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/docs/HOST-CONTRACT.md +26 -1
- package/package.json +1 -1
- package/server/handler.js +35 -7
- package/server/page-mur.js +9 -3
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -512,6 +512,30 @@ said the player holds no server secret at all — too absolute: the standalone c
|
|
|
512
512
|
The player's counters bound the *rate*; your plugin bounds the *code*. Both are needed, and neither
|
|
513
513
|
replaces the other.
|
|
514
514
|
|
|
515
|
+
## Who may open a restricted document (`plugins.documentAccess`, 0.1.172)
|
|
516
|
+
|
|
517
|
+
The visitor wall answers one question: *has this person proven an address?* A host that restricts a document to
|
|
518
|
+
**its own team**, or to one partner organisation, could not express it — any proven address opened it. Provide
|
|
519
|
+
`plugins.documentAccess` with:
|
|
520
|
+
|
|
521
|
+
```js
|
|
522
|
+
decide({ share, visitor }) → Promise<{ ok: true } | { ok: false, reason?: "denied" | "unavailable" }>
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
It is called for a document whose link carries `require_auth`, once the visitor is signed in (`visitor` is what
|
|
526
|
+
`plugins.visitors.currentVisitor(req)` returned). It is **not** called for an open document, nor before the visitor
|
|
527
|
+
signs in — the wall handles that.
|
|
528
|
+
|
|
529
|
+
| your answer | what the reader gets |
|
|
530
|
+
|---|---|
|
|
531
|
+
| `{ ok: true }` | the document |
|
|
532
|
+
| `{ ok: false }` | the wall again, saying *this address* has no access, so they can sign in with the one the document was sent to; `?file=1` answers `403 { error: "denied" }` without streaming; embedded, the bridge reports `denied` |
|
|
533
|
+
| an exception, anything unreadable, or `{ ok: false, reason: "unavailable" }` | a **refusal** (`auth-unavailable`; `?file=1` → `503`) — never an opening. The exception goes to `errors.capture` |
|
|
534
|
+
|
|
535
|
+
Without the plugin, nothing changes: a proven address opens the document, as before. ⚠️ **That is also why its
|
|
536
|
+
absence is silent** — a host that upgrades the player without wiring the plugin keeps the old behaviour and sees no
|
|
537
|
+
error. Test that your host actually provides it.
|
|
538
|
+
|
|
515
539
|
## ⚠️ What `limits.allow` promises changed
|
|
516
540
|
|
|
517
541
|
It used to promise *best effort, per process*. The standalone context now counts in a **shared
|
|
@@ -919,7 +943,8 @@ closed.
|
|
|
919
943
|
|---|---|---|
|
|
920
944
|
| `revoked` | unknown or revoked link | do not open |
|
|
921
945
|
| `auth-required` | restricted document, visitor not signed in | do not open — the wall stays up |
|
|
922
|
-
| `auth-unavailable` | restricted document, access wall missing from this instance | do not open |
|
|
946
|
+
| `auth-unavailable` | restricted document, access wall missing from this instance — or the host's `documentAccess` plugin failed | do not open |
|
|
947
|
+
| `denied` | restricted document, visitor signed in but the host's `documentAccess` plugin says this address has no access (0.1.172) | do not open — the wall stays up and offers another address |
|
|
923
948
|
| `expired` | the link's expiry date has passed (migration `0028`) | do not open — the page tells the reader to ask for a new link |
|
|
924
949
|
| `password-required` | the link is password-protected and this browser has not entered it (migration `0028`) | do not open — the password page stays up in the frame |
|
|
925
950
|
| `ended` | presentation over or unknown | do not open |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.172",
|
|
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",
|
package/server/handler.js
CHANGED
|
@@ -9,7 +9,7 @@ const crypto = require("crypto");
|
|
|
9
9
|
const { capturerSansBloquer } = require("./capture");
|
|
10
10
|
const { Readable } = require("node:stream");
|
|
11
11
|
const { pipeline } = require("node:stream/promises");
|
|
12
|
-
const {
|
|
12
|
+
const { resoudreLien } = require("./shares");
|
|
13
13
|
const { PRESENT_QUOTA_PER_HOUR, PRESENT_CACHE_MS, estSlug } = require("./shared.generated.js");
|
|
14
14
|
const { creerCache, CODE_SATURATION } = require("./cache.js");
|
|
15
15
|
const mesures = require("./mesures.js");
|
|
@@ -1145,13 +1145,17 @@ async function handlerMesure(req, res) {
|
|
|
1145
1145
|
embed ? embedFrameAncestors() : "'self'");
|
|
1146
1146
|
}
|
|
1147
1147
|
|
|
1148
|
-
|
|
1148
|
+
// ⚠️ UNE LECTURE, PAS DEUX. 0.1.170 demandait le lien à `getShareBySlug`, puis, quand il ne s'ouvrait pas,
|
|
1149
|
+
// le RELISAIT par `resoudreLien` pour dire pourquoi — la même ligne, deux allers-retours, et sur le chemin le plus
|
|
1150
|
+
// fréquent des refus (un lien révoqué qui circule encore). `resoudreLien` rend les deux à la fois.
|
|
1151
|
+
const lu = slug ? await resoudreLien(slug, req) : { share: null, refus: "revoked", ligne: null };
|
|
1152
|
+
const share = lu.share;
|
|
1149
1153
|
if (!share) {
|
|
1150
1154
|
// ⚠️ UN LIEN PROTÉGÉ (0028) NE SE CONFOND PAS AVEC UN LIEN RÉVOQUÉ. Expiré, il le DIT — la
|
|
1151
1155
|
// personne peut en demander un autre ; fermé par un mot de passe, il le DEMANDE. Et dans les
|
|
1152
1156
|
// deux cas le FICHIER reste derrière : servir la page du mot de passe en laissant `?file=1`
|
|
1153
1157
|
// streamer le PDF serait une porte de décor (la leçon de `murDocument.test.js`).
|
|
1154
|
-
const { refus, ligne } =
|
|
1158
|
+
const { refus, ligne } = lu;
|
|
1155
1159
|
if (refus === "expired") {
|
|
1156
1160
|
if (String(q.file || "") === "1") { repondreJson(res, 410, { ok: false, error: "expired" }); return; }
|
|
1157
1161
|
if (!embed) return sendHtml(res, 410, lienExpireHtml(ligne));
|
|
@@ -1175,8 +1179,31 @@ async function handlerMesure(req, res) {
|
|
|
1175
1179
|
if (share.require_auth === true && !visitors) return sendRefusal(res, "auth-unavailable", embed);
|
|
1176
1180
|
const gated = share.require_auth === true && !visitor;
|
|
1177
1181
|
|
|
1182
|
+
// L'ACCÈS PAR DOCUMENT (0.1.172) — greffon facultatif `documentAccess`. Le mur ne savait dire qu'une chose :
|
|
1183
|
+
// « cette personne a prouvé son adresse ». Un hôte qui réserve un document à SON équipe, ou à une organisation,
|
|
1184
|
+
// ne pouvait pas l'exprimer : toute adresse prouvée l'ouvrait. Le greffon répond, pour un document réservé et un
|
|
1185
|
+
// visiteur identifié, « oui » ou « non » — c'est l'HÔTE qui sait qui a droit à quoi.
|
|
1186
|
+
// · absent : l'ancien comportement (une adresse prouvée suffit) — rien ne change pour un hôte qui ne l'a pas ;
|
|
1187
|
+
// · « non » : le mur revient, en disant que CETTE adresse n'a pas accès, et propose d'en utiliser une autre ;
|
|
1188
|
+
// · une panne (exception, réponse illisible, `reason: "unavailable"`) : REFUS — jamais d'ouverture par défaut.
|
|
1189
|
+
let refusAcces = null;
|
|
1190
|
+
if (share.require_auth === true && visitor && PLAYER.plugins.documentAccess) {
|
|
1191
|
+
let d = null;
|
|
1192
|
+
try { d = await PLAYER.plugins.documentAccess.decide({ share, visitor }); } catch (e) {
|
|
1193
|
+
try { await PLAYER.errors.capture(e, { route: "doc", etape: "documentAccess" }); } catch { /* la capture ne décide rien */ }
|
|
1194
|
+
}
|
|
1195
|
+
if (!d || d.ok !== true) refusAcces = d && d.ok === false && d.reason !== "unavailable" ? "denied" : "auth-unavailable";
|
|
1196
|
+
}
|
|
1197
|
+
if (refusAcces === "auth-unavailable") {
|
|
1198
|
+
if (String(q.file || "") === "1") { repondreJson(res, 503, { ok: false, error: "auth-unavailable" }); return; }
|
|
1199
|
+
return sendRefusal(res, "auth-unavailable", embed);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1178
1202
|
if (String(q.file || "") === "1") {
|
|
1179
1203
|
if (gated) { repondreJson(res, 401, { ok: false, error: "auth" }); return; }
|
|
1204
|
+
// Le FICHIER reste derrière un refus d'accès, comme derrière le mur : une page qui dit non en laissant ?file=1
|
|
1205
|
+
// streamer le PDF serait une porte de décor.
|
|
1206
|
+
if (refusAcces === "denied") { repondreJson(res, 403, { ok: false, error: "denied" }); return; }
|
|
1180
1207
|
// Stream depuis le Storage en RELAYANT les requêtes Range → pdf.js charge progressivement (les 1res
|
|
1181
1208
|
// pages s'affichent sans télécharger tout le PDF) → affichage bien plus rapide.
|
|
1182
1209
|
const range = req.headers["range"];
|
|
@@ -1189,15 +1216,16 @@ async function handlerMesure(req, res) {
|
|
|
1189
1216
|
|
|
1190
1217
|
// Soft wall : contenu réservé → on sert la page de connexion visiteur (email + code)
|
|
1191
1218
|
// AVANT de charger le lecteur. À la vérification, le cookie est posé → un reload lève le mur.
|
|
1192
|
-
if (gated) {
|
|
1219
|
+
if (gated || refusAcces === "denied") {
|
|
1193
1220
|
let wlogo = ""; try { wlogo = await PLAYER.branding.logo(); } catch { /* sans logo */ }
|
|
1194
1221
|
const wnonce = crypto.randomBytes(16).toString("base64");
|
|
1195
1222
|
const gcid = visitors.googleClientId();
|
|
1196
1223
|
// Intégré : le mur reste affiché (le visiteur peut s'y connecter sur place) mais il DIT à
|
|
1197
1224
|
// l'hôte que le document est retenu — sinon l'hôte croit à une panne et replie sur son
|
|
1198
|
-
// lecteur, qui lui ouvrirait le document que ce mur protège.
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1225
|
+
// lecteur, qui lui ouvrirait le document que ce mur protège. Refusé à CETTE adresse : `denied`.
|
|
1226
|
+
const raison = refusAcces === "denied" ? "denied" : "auth-required";
|
|
1227
|
+
const wall = softWallHtml(share, wnonce, wlogo, gcid, refusAcces === "denied" ? { refuse: String(visitor.email || "") } : null)
|
|
1228
|
+
+ (embed ? `<script nonce="${wnonce}">try{parent.postMessage({type:"3dd-doc-embed-denied",reason:${jsonPourScript(raison)}},"*")}catch(e){}</script>` : "");
|
|
1201
1229
|
return sendSoftWallHtml(res, wall, wnonce, [originOf(wlogo), originOf(share.brand_logo)].filter(Boolean).join(" "), embed ? embedFrameAncestors() : null);
|
|
1202
1230
|
}
|
|
1203
1231
|
|
package/server/page-mur.js
CHANGED
|
@@ -14,7 +14,10 @@ function notFoundHtml() {
|
|
|
14
14
|
// Page « soft wall » : accès à un document réservé (require_auth). On ne demande PAS un compte,
|
|
15
15
|
// on propose de RECEVOIR le document — l'email est l'action pour débloquer, pas un péage.
|
|
16
16
|
// Email → code à 6 chiffres → cookie posé → reload → le lecteur s'ouvre. (Google = Lot B.)
|
|
17
|
-
function softWallHtml(share, nonce, logoUrl, googleClientId) {
|
|
17
|
+
function softWallHtml(share, nonce, logoUrl, googleClientId, opts) {
|
|
18
|
+
// Refusé à CETTE adresse (greffon `documentAccess`, 0.1.172) : on le dit, et on garde le formulaire — l'issue est
|
|
19
|
+
// de s'identifier avec l'adresse à laquelle le document a été envoyé.
|
|
20
|
+
const refuse = opts && typeof opts.refuse === "string" ? opts.refuse : null;
|
|
18
21
|
const title = esc(share.doc_title || share.file_name || "ce document");
|
|
19
22
|
const brandLogo = esc(share.brand_logo || "");
|
|
20
23
|
const dark = !!share.brand_dark;
|
|
@@ -56,8 +59,11 @@ function softWallHtml(share, nonce, logoUrl, googleClientId) {
|
|
|
56
59
|
<body>
|
|
57
60
|
<div class=card>
|
|
58
61
|
${logo ? `<img class=logo src="${logo}" alt="${logoAlt}">` : ""}
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
${refuse !== null
|
|
63
|
+
? `<h1>Ce document vous est réservé</h1>
|
|
64
|
+
<p class=sub><span class=doc>${title}</span><br>${refuse ? `L'adresse ${esc(refuse)} n'y a pas accès.` : "Cette adresse n'y a pas accès."} Identifiez-vous avec l'adresse à laquelle il vous a été envoyé.</p>`
|
|
65
|
+
: `<h1>Accédez à votre document</h1>
|
|
66
|
+
<p class=sub><span class=doc>${title}</span><br>Débloquez-le en un instant.</p>`}
|
|
61
67
|
|
|
62
68
|
${gcid ? `<div id=gbtn class=gbtn></div><div class=orsep><span>ou par email</span></div>` : ""}
|
|
63
69
|
<div id=s1>
|