discovery-media-player 0.1.66 → 0.1.67

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.66",
3
+ "version": "0.1.67",
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
@@ -7,7 +7,7 @@ const crypto = require("crypto");
7
7
  const { Readable } = require("node:stream");
8
8
  const { pipeline } = require("node:stream/promises");
9
9
  const { getShareBySlug, logView, upsertSession, createReshare, sendReshareEmail, upsertInternalSession,
10
- createShare, revokeShare, setShareAuth, overview: docOverview, listSharesForDoc, listSessionsForDoc, internalStatsForDoc } = require("./shares");
10
+ createShare, revokeShare, setShareAuth, overview: docOverview, listSharesForDoc, listSessionsForDoc, internalStatsForDoc , cleIdempotence } = require("./shares");
11
11
  const { SESSION_QUOTA_PER_HOUR, PRESENT_QUOTA_PER_HOUR, PRESENT_CACHE_MS } = require("./shared.generated.js");
12
12
  const { creerCache } = require("./cache.js");
13
13
 
@@ -847,7 +847,7 @@ var Map3DD=(function(){
847
847
  var svb=document.getElementById('mapSV'); if(svb&&!svb._w){svb._w=1;svb.addEventListener('click',function(){ if(!map)return; var c=useG?[map.getCenter().lat(),map.getCenter().lng()]:[map.getCenter().lat,map.getCenter().lng]; goSV(c); });}
848
848
  var tm=document.getElementById('mapToMap'); if(tm&&!tm._w){tm._w=1;tm.addEventListener('click',toMap);} }
849
849
  function doSearch(text){ text=(text||'').trim(); var res=document.getElementById('mapRes'); if(!text||!res)return; res.innerHTML='<div style="padding:9px 14px;color:#888;font-size:12px">Recherche…</div>';
850
- fetch('https://nominatim.openstreetmap.org/search?format=json&limit=5&q='+encodeURIComponent(text),{headers:{Accept:'application/json'}}).then(function(r){return r.json();}).then(function(l){ if(!l||!l.length){res.innerHTML='<div style="padding:9px 14px;color:#888;font-size:12px">Aucun résultat.</div>';return;} res.innerHTML=l.map(function(o){return '<button data-lat="'+o.lat+'" data-lng="'+o.lon+'">'+String(o.display_name||'').replace(/</g,'&lt;')+'</button>';}).join(''); }).catch(function(){res.innerHTML='<div style="padding:9px 14px;color:#c0392b;font-size:12px">Recherche indisponible.</div>';}); }
850
+ fetch('https://nominatim.openstreetmap.org/search?format=json&limit=5&q='+encodeURIComponent(text),{headers:{Accept:'application/json'}}).then(function(r){return r.json();}).then(function(l){ if(!l||!l.length){res.innerHTML='<div style="padding:9px 14px;color:#888;font-size:12px">Aucun résultat.</div>';return;} res.innerHTML=l.map(function(o){var la=Number(o.lat),lo=Number(o.lon);if(!isFinite(la)||!isFinite(lo))return '';return '<button data-lat="'+la+'" data-lng="'+lo+'">'+String(o.display_name||'').replace(/</g,'&lt;')+'</button>';}).join(''); }).catch(function(){res.innerHTML='<div style="padding:9px 14px;color:#c0392b;font-size:12px">Recherche indisponible.</div>';}); }
851
851
  // ── Street View (Google) ─────────────────────────────────────────────────────────────────────────
852
852
  function tempHint(t){ var h=document.getElementById('mapHint'); if(h){h.textContent=t; setTimeout(function(){ if(h.textContent===t)h.textContent=(isPres?'':'Vue du présentateur — en direct'); },2600);} }
853
853
  // Le présentateur passe en Street View depuis le centre de la carte : on cherche le panorama le plus proche.
@@ -3600,7 +3600,7 @@ async function handler(req, res) {
3600
3600
  const dejaLa = await PLAYER.db.request(
3601
3601
  `commercial_doc_shares?doc_id=eq.${encodeURIComponent(docId)}&created_by=is.null&recipient_email=is.null${filtreAtteste}&select=slug&limit=1`,
3602
3602
  );
3603
- const cleHote = `hote:${docId}|${atteste || ""}`;
3603
+ const cleHote = cleIdempotence("hote", [docId, atteste || ""]);
3604
3604
  if (Array.isArray(dejaLa) && dejaLa[0]) {
3605
3605
  // Réemploi d'une ligne historique (sans clé) : on lui POSE la clé au passage — les
3606
3606
  // demandes suivantes la trouveront par l'unicité, et les doublons d'avant 0011
@@ -3674,7 +3674,7 @@ async function handler(req, res) {
3674
3674
  const docId = String(body.docId || "");
3675
3675
  if (!docId || !body.fileUrl) return jd(400, { ok: false, error: "docId/fileUrl requis" });
3676
3676
  const ex = await PLAYER.db.request(`commercial_doc_shares?doc_id=eq.${encodeURIComponent(docId)}&is_test=eq.true&select=slug&limit=1`);
3677
- const cleTest = `repetition:${docId}`;
3677
+ const cleTest = cleIdempotence("repetition", [docId]);
3678
3678
  if (Array.isArray(ex) && ex[0]) {
3679
3679
  const cleDispo = await require("./schema").attendue("liensUniques");
3680
3680
  await PLAYER.db.request(`commercial_doc_shares?slug=eq.${encodeURIComponent(ex[0].slug)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { doc_title: body.docTitle || null, file_url: String(body.fileUrl), file_name: body.fileName || null, bot_enabled: true, bot_guided: true, bot_profile_id: (body.profileId || "").trim() || null, revoked: false, ...(cleDispo ? { idem_key: cleTest } : {}) } });
@@ -3924,8 +3924,13 @@ async function handler(req, res) {
3924
3924
  // session casserait la page avant qu'elle existe. L'URL porte la version → cache immuable.
3925
3925
  if (req.method === "GET" && (String(q.asset || "") === "pdf" || String(q.asset || "") === "pdfworker")) {
3926
3926
  const fichier = q.asset === "pdf" ? "pdfjs-dist/build/pdf.min.mjs" : "pdfjs-dist/build/pdf.worker.min.mjs";
3927
- let octets = null;
3928
- try { octets = require("node:fs").readFileSync(require.resolve(fichier)); } catch { /* dépendance absente */ }
3927
+ // Lu UNE fois par processus : la dépendance est épinglée exacte, les octets ne changent
3928
+ // qu'avec un déploiement — relire 1,7 Mo sur disque à chaque requête ne prouvait rien.
3929
+ if (!global.__actifsPdfjs) global.__actifsPdfjs = Object.create(null);
3930
+ let octets = global.__actifsPdfjs[fichier] || null;
3931
+ if (!octets) {
3932
+ try { octets = global.__actifsPdfjs[fichier] = require("node:fs").readFileSync(require.resolve(fichier)); } catch { /* dépendance absente */ }
3933
+ }
3929
3934
  if (!octets) { res.statusCode = 404; res.end("actif indisponible"); return; }
3930
3935
  res.statusCode = 200;
3931
3936
  res.setHeader("Content-Type", "text/javascript; charset=utf-8");
package/server/shares.js CHANGED
@@ -10,6 +10,17 @@ function init(ctx) { PLAYER = ctx; }
10
10
 
11
11
 
12
12
  const enc = encodeURIComponent;
13
+
14
+ // ⚠️ LA CLÉ D'IDEMPOTENCE EST UNE EMPREINTE, PAS UNE CONCATÉNATION (sixième audit). La forme
15
+ // historique `hote:<docId>|<email>` portait trois défauts : tronquée à 300 caractères à
16
+ // l'ÉCRITURE mais relue ENTIÈRE après un 409 — le perdant légitime finissait en 500 faute de
17
+ // trouver son gagnant ; deux docId partageant le préfixe se volaient leur lien ; et un `|` dans
18
+ // une composante déplaçait la frontière. JSON.stringify fixe les frontières, sha256 fixe la
19
+ // longueur (genre + 65 caractères, loin de toute limite). Une seule fonction pour ÉCRIRE et
20
+ // RELIRE : les deux exemplaires du fait ne peuvent plus diverger. Les clés ancien format en
21
+ // base s'éteignent d'elles-mêmes — le réemploi passe par doc_id et re-pose la clé canonique.
22
+ const cleIdempotence = (genre, composants) =>
23
+ genre + ":" + crypto.createHash("sha256").update(JSON.stringify(composants)).digest("hex");
13
24
  const low = (s) => String(s || "").trim().toLowerCase();
14
25
  function newSlug() { return crypto.randomBytes(9).toString("base64url"); } // ~12 chars URL-safe
15
26
 
@@ -23,7 +34,7 @@ async function createShare({ docId, docTitle, fileUrl, fileName, recipientEmail,
23
34
  // CRÉATION de liens. Même sonde que la clé des messages (0005).
24
35
  const cleDispo = idemKey ? await require("./schema").attendue("liensUniques") : false;
25
36
  const row = {
26
- ...(cleDispo ? { idem_key: String(idemKey).slice(0, 300) } : {}),
37
+ ...(cleDispo ? { idem_key: String(idemKey) } : {}),
27
38
  slug, doc_id: String(docId), doc_title: docTitle || null, file_url: String(fileUrl), file_name: fileName || null,
28
39
  recipient_email: low(recipientEmail) || null, recipient_name: (recipientName || "").trim() || null, created_by: low(createdBy) || null,
29
40
  // ⚠️ DEUX CHAMPS, DEUX FAITS. `recipient_email` dit qui a le droit d'EXPÉDIER en son nom au
@@ -450,4 +461,5 @@ async function internalStatsForDoc(docId) {
450
461
  return { opens: list.length, readers: users.length, lastAt: list[0]?.last_at || null, users };
451
462
  }
452
463
 
453
- module.exports = { init, createShare, createReshare, sendReshareEmail, getShareBySlug, logView, upsertSession, listSharesForDoc, listSessionsForDoc, revokeShare, setShareAuth, overview, upsertInternalSession, internalStatsForDoc };
464
+ module.exports = {
465
+ cleIdempotence, init, createShare, createReshare, sendReshareEmail, getShareBySlug, logView, upsertSession, listSharesForDoc, listSessionsForDoc, revokeShare, setShareAuth, overview, upsertInternalSession, internalStatsForDoc };
package/supabase/init.sql CHANGED
@@ -59,6 +59,12 @@ create table if not exists public.commercial_doc_shares (
59
59
  attested_recipient_email text
60
60
  );
61
61
  create index if not exists cds_doc_id_idx on public.commercial_doc_shares (doc_id);
62
+ -- ⚠️ RÈGLE (sixième audit) : tout index sur une colonne apparue APRÈS un init déjà publié est
63
+ -- précédé de son ALTER, ICI MÊME. Le rattrapage de fin de fichier ne suffit pas : sur une base
64
+ -- d'hier, `create table if not exists` saute le corps de table, et l'index s'exécutait AVANT le
65
+ -- rattrapage — le rejeu cassait sur cette ligne. Garde : scénario CI « init v0.1.64 → rejeu ».
66
+ alter table public.commercial_doc_shares
67
+ add column if not exists idem_key text;
62
68
  create unique index if not exists cds_idem_key_uniq
63
69
  on public.commercial_doc_shares (idem_key)
64
70
  where idem_key is not null;
@@ -190,6 +196,9 @@ create table if not exists public.doc_presentation_messages (
190
196
  create index if not exists dpm_slug_idx on public.doc_presentation_messages (slug, created_at);
191
197
  -- ⚠️ Portée sur (slug, client_key), pas sur la clé seule : deux présentations n'ont aucune raison
192
198
  -- de partager un espace de clés. Partiel, pour que les lignes sans clé ne se gênent pas entre elles.
199
+ -- Même règle que cds_idem_key_uniq : la colonne est assurée avant son index.
200
+ alter table public.doc_presentation_messages
201
+ add column if not exists client_key text;
193
202
  create unique index if not exists dpm_client_key_uniq
194
203
  on public.doc_presentation_messages (slug, client_key)
195
204
  where client_key is not null;