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.
@@ -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 b = await appelHote(env.PLAYER_HOST_BRAND_URL, secret, { key: String(key) }, journal);
798
- return b && b.logo ? { logo: String(b.logo), name: String(b.name || ""), dark: !!b.dark } : null;
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) {
@@ -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.172",
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": "^4.1.11",
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.9.0",
93
- "jsdom": "^30.0.1",
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": "^4.1.10",
99
- "yaml": "2.9.0"
98
+ "vitest": "^5.0.2",
99
+ "yaml": "2.9.1"
100
100
  },
101
101
  "dependencies": {
102
- "pdfjs-dist": "6.2.108"
102
+ "pdfjs-dist": "6.3.289"
103
103
  }
104
104
  }
@@ -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
- return `<div class=lgl>${mesure}${liens ? `<span class=lgl-links>${liens}</span>` : ""}</div>`;
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
- .lgl{position:fixed;left:0;right:0;bottom:0;z-index:5;display:flex;flex-wrap:wrap;gap:4px 10px;
38
- align-items:center;justify-content:center;padding:5px 12px;font-size:10.5px;line-height:1.35;
39
- color:rgba(255,255,255,.94);background:rgba(0,0,0,.66);backdrop-filter:blur(3px);pointer-events:none}
40
- .lgl a{color:inherit;text-decoration:underline;text-underline-offset:2px;pointer-events:auto}
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-note{opacity:1}
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 };