discovery-media-player 0.1.172 → 0.1.174
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/context/standalone.js +34 -2
- package/docs/HOST-CONTRACT.md +6 -0
- package/package.json +7 -7
- package/server/gabarit-legal.js +40 -7
package/context/standalone.js
CHANGED
|
@@ -118,6 +118,22 @@ function fetchBorne(cible, options = {}, delaiMs) {
|
|
|
118
118
|
*/
|
|
119
119
|
const DELAI_AUTH_MS = 5000;
|
|
120
120
|
const DELAI_ROUTE_HOTE_MS = 4000;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* La marque d'un client, GARDÉE EN MÉMOIRE (#570). Chaque ouverture d'un lien tracé attendait un
|
|
124
|
+
* POST à l'hôte avant d'envoyer la page ; hôte lent ou en panne, chaque ouverture payait jusqu'à
|
|
125
|
+
* `DELAI_ROUTE_HOTE_MS`. Une marque change rarement, un lien s'ouvre souvent.
|
|
126
|
+
*
|
|
127
|
+
* ⚠️ LES DURÉES DISENT CE QUI RESTE VRAI DE `brands.js` : la marque est toujours résolue par
|
|
128
|
+
* l'hôte, jamais recopiée dans le lien — une charte corrigée se voit en 5 minutes au plus. Une
|
|
129
|
+
* absence (clé inconnue, hôte en panne) n'est gardée qu'une minute : assez pour ne plus payer le
|
|
130
|
+
* délai à chaque ouverture, assez peu pour qu'une marque créée entre-temps apparaisse vite.
|
|
131
|
+
* ⚠️ BORNÉE : les clés viennent des liens, donc de la base ; sans plafond, une instance qui sert
|
|
132
|
+
* beaucoup de clients garderait tout pour toujours. La plus ancienne entrée sort la première.
|
|
133
|
+
*/
|
|
134
|
+
const MARQUE_GARDEE_MS = 5 * 60_000;
|
|
135
|
+
const MARQUE_ABSENTE_MS = 60_000;
|
|
136
|
+
const MARQUES_MAX = 500;
|
|
121
137
|
const DELAI_STOCKAGE_MS = 15000;
|
|
122
138
|
const DELAI_BASE_MS = 15000;
|
|
123
139
|
|
|
@@ -431,6 +447,8 @@ function createStandaloneContext(env = process.env) {
|
|
|
431
447
|
const origins = () => storage.storageOrigins(env);
|
|
432
448
|
const hostBase = () => storage.hostFetchBase(env);
|
|
433
449
|
const root = () => storage.localRoot(env);
|
|
450
|
+
// Les marques des clients, propres à CE contexte (#570) : deux contextes ne partagent rien.
|
|
451
|
+
const marques = new Map();
|
|
434
452
|
|
|
435
453
|
return {
|
|
436
454
|
// Aucun greffon : l'assistant IA, l'intro de marque et les comptes visiteurs sont des produits
|
|
@@ -794,8 +812,22 @@ function createStandaloneContext(env = process.env) {
|
|
|
794
812
|
*/
|
|
795
813
|
async forKey(key) {
|
|
796
814
|
if (!key) return null;
|
|
797
|
-
const
|
|
798
|
-
|
|
815
|
+
const cle = String(key);
|
|
816
|
+
const gardee = marques.get(cle);
|
|
817
|
+
// Une promesse, pas une valeur : dix lecteurs qui ouvrent le même lien au même instant
|
|
818
|
+
// attendent le MÊME appel au lieu d'en lancer dix.
|
|
819
|
+
if (gardee && Date.now() - gardee.depuis < gardee.duree) return gardee.marque;
|
|
820
|
+
const entree = { depuis: Date.now(), duree: MARQUE_GARDEE_MS, marque: null };
|
|
821
|
+
entree.marque = appelHote(env.PLAYER_HOST_BRAND_URL, secret, { key: cle }, journal).then((b) => {
|
|
822
|
+
const m = b && b.logo ? { logo: String(b.logo), name: String(b.name || ""), dark: !!b.dark } : null;
|
|
823
|
+
if (!m) entree.duree = MARQUE_ABSENTE_MS;
|
|
824
|
+
return m;
|
|
825
|
+
});
|
|
826
|
+
// Réinsérée en dernier : l'ordre d'insertion de la Map est l'ordre de sortie.
|
|
827
|
+
marques.delete(cle);
|
|
828
|
+
marques.set(cle, entree);
|
|
829
|
+
if (marques.size > MARQUES_MAX) marques.delete(marques.keys().next().value);
|
|
830
|
+
return entree.marque;
|
|
799
831
|
},
|
|
800
832
|
|
|
801
833
|
title(base, qualificatif) {
|
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -842,6 +842,12 @@ The link carries a **reference**, never a copy of the logo: a tracked link lives
|
|
|
842
842
|
inbox, and a logo frozen at send time would not follow a corrected brand. `name` is not decorative
|
|
843
843
|
— it is what shows when the logo fails to load.
|
|
844
844
|
|
|
845
|
+
The standalone context **keeps each answer in memory, per process**, so that opening a link does not
|
|
846
|
+
wait on your route every time: a brand for **5 minutes**, an absence (unknown key, error, timeout)
|
|
847
|
+
for **one minute**, at most 500 keys, oldest out first. Concurrent openings of the same link share
|
|
848
|
+
one call. So a corrected brand shows within 5 minutes, and a brand created for a key that was
|
|
849
|
+
unknown shows within a minute. The link still carries only the key — nothing is frozen in it.
|
|
850
|
+
|
|
845
851
|
### 3. Serving a file the player cannot reach
|
|
846
852
|
|
|
847
853
|
If your documents sit behind an API key, the player must **never** hold it. Expose one route
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.174",
|
|
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",
|
|
@@ -83,22 +83,22 @@
|
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@eslint/js": "^10.0.1",
|
|
86
|
-
"@vitest/coverage-v8": "^
|
|
86
|
+
"@vitest/coverage-v8": "^5.0.2",
|
|
87
87
|
"ajv": "8.20.0",
|
|
88
88
|
"axe-core": "^4.13.0",
|
|
89
89
|
"dockerfile-ast": "0.7.1",
|
|
90
90
|
"esbuild": "^0.28.2",
|
|
91
91
|
"eslint": "^10.8.1",
|
|
92
|
-
"fast-check": "4.
|
|
93
|
-
"jsdom": "^30.
|
|
92
|
+
"fast-check": "4.10.2",
|
|
93
|
+
"jsdom": "^30.1.2",
|
|
94
94
|
"playwright-core": "^1.62.1",
|
|
95
95
|
"semver": "^7.8.5",
|
|
96
96
|
"typescript": "^5.5.4",
|
|
97
97
|
"typescript-eslint": "^8.46.4",
|
|
98
|
-
"vitest": "^
|
|
99
|
-
"yaml": "2.9.
|
|
98
|
+
"vitest": "^5.0.2",
|
|
99
|
+
"yaml": "2.9.1"
|
|
100
100
|
},
|
|
101
101
|
"dependencies": {
|
|
102
|
-
"pdfjs-dist": "6.
|
|
102
|
+
"pdfjs-dist": "6.3.289"
|
|
103
103
|
}
|
|
104
104
|
}
|
package/server/gabarit-legal.js
CHANGED
|
@@ -30,18 +30,51 @@ function legalFooter({ tracked, sansExpediteur }) {
|
|
|
30
30
|
const texte = sansExpediteur ? (L.trackingNoticeAnonymous || L.trackingNotice) : L.trackingNotice;
|
|
31
31
|
const mesure = tracked ? `<span class=lgl-note>${esc(texte)}</span>` : "";
|
|
32
32
|
if (!liens && !mesure) return "";
|
|
33
|
-
|
|
33
|
+
// ⚠️ UNE PASTILLE REPLIÉE, PLUS UNE BARRE (06/10/2026, à la demande d'un hôte). La barre noire
|
|
34
|
+
// pleine largeur recouvrait le bas de chaque page pour redire trois choses qu'on ne lit qu'une
|
|
35
|
+
// fois. Repliée, la pastille garde VISIBLE ce qui doit l'être sans geste — « Lecture mesurée »,
|
|
36
|
+
// le résumé de la mention due au lecteur — et la phrase exacte comme les liens (le code source
|
|
37
|
+
// que l'AGPL doit à l'utilisateur) restent dans la page, à un clic. Rien n'est retiré du HTML :
|
|
38
|
+
// seule la présentation change.
|
|
39
|
+
//
|
|
40
|
+
// ⚠️ `<details>`, SANS SCRIPT : la page tourne sous une CSP à nonce, et un pied qui aurait besoin
|
|
41
|
+
// de JavaScript pour s'ouvrir resterait fermé — mention comprise — le jour où son script ne
|
|
42
|
+
// passerait pas.
|
|
43
|
+
//
|
|
44
|
+
// ⚠️ « Informations » quand on ne mesure pas (aperçu interne) : écrire « Lecture mesurée » sur
|
|
45
|
+
// une page qui ne mesure rien serait faux dans l'autre sens.
|
|
46
|
+
const resume = tracked ? "Lecture mesurée" : "Informations";
|
|
47
|
+
return `<details class=lgl><summary class=lgl-pill>${ICONE_INFO}<span>${resume}</span></summary>`
|
|
48
|
+
+ `<div class=lgl-panel>${mesure}${liens ? `<span class=lgl-links>${liens}</span>` : ""}</div></details>`;
|
|
34
49
|
}
|
|
35
50
|
|
|
51
|
+
// Un « i » cerclé, décoratif : le mot à côté suffit à un lecteur d'écran.
|
|
52
|
+
const ICONE_INFO = `<svg aria-hidden=true viewBox="0 0 24 24" width=13 height=13 fill=none stroke=currentColor stroke-width=2 stroke-linecap=round><circle cx=12 cy=12 r=9.5 /><path d="M12 11v5.5M12 7.6v.1"/></svg>`;
|
|
53
|
+
|
|
36
54
|
const LEGAL_CSS = `
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
.lgl
|
|
55
|
+
/* ⚠️ EN BAS À GAUCHE, PAS À DROITE : le coin droit est déjà pris par la bulle de l'assistant,
|
|
56
|
+
celle du chat en direct et « Propulsé par » — trois occupants qu'une pastille de plus
|
|
57
|
+
viendrait chevaucher. Au-dessus des pages (z-index 5), sous le tiroir de vignettes (7). */
|
|
58
|
+
.lgl{position:fixed;left:calc(10px + env(safe-area-inset-left));bottom:calc(10px + env(safe-area-inset-bottom));
|
|
59
|
+
z-index:5;max-width:min(92vw,420px);font-size:11px;line-height:1.4;color:rgba(255,255,255,.94)}
|
|
60
|
+
/* La colonne de vignettes, ouverte en grand écran, pousse le document : la pastille la suit
|
|
61
|
+
au lieu de se poser sur la dernière vignette. */
|
|
62
|
+
@media (min-width:861px){ body.vign-on .lgl{left:calc(178px + env(safe-area-inset-left))} }
|
|
63
|
+
.lgl-pill{display:inline-flex;align-items:center;gap:5px;padding:4px 9px 4px 7px;border-radius:999px;
|
|
64
|
+
background:rgba(20,18,14,.74);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
|
|
65
|
+
cursor:pointer;list-style:none;user-select:none}
|
|
66
|
+
/* ⚠️ DISCRÈTE PAR LA TAILLE, JAMAIS PAR L'OPACITÉ : la pastille flotte sur la page du
|
|
67
|
+
document, souvent blanche, et un texte atténué y passerait sous le contraste exigé. Le fond
|
|
68
|
+
à .74 garde le blanc à ~7:1 même sur une page blanche. */
|
|
69
|
+
.lgl-pill::-webkit-details-marker{display:none}
|
|
70
|
+
.lgl-pill:hover,.lgl[open] .lgl-pill{background:rgba(20,18,14,.88)}
|
|
71
|
+
.lgl-pill:focus-visible{outline:2px solid #fff;outline-offset:2px}
|
|
72
|
+
.lgl-panel{display:flex;flex-direction:column;gap:5px;margin-top:6px;padding:9px 11px;border-radius:10px;
|
|
73
|
+
background:rgba(20,18,14,.86);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px)}
|
|
74
|
+
.lgl a{color:inherit;text-decoration:underline;text-underline-offset:2px}
|
|
41
75
|
.lgl a:hover{color:#fff}
|
|
42
76
|
.lgl-sep{margin:0 6px;opacity:.8}
|
|
43
|
-
.lgl-
|
|
44
|
-
@media (max-width:640px){ .lgl{font-size:10px;padding:4px 10px} }
|
|
77
|
+
@media (max-width:640px){ .lgl{font-size:10.5px} }
|
|
45
78
|
`;
|
|
46
79
|
|
|
47
80
|
module.exports = { init, legalFooter, LEGAL_CSS };
|