discovery-media-player 0.1.69 → 0.1.70

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.69",
3
+ "version": "0.1.70",
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",
@@ -55,12 +55,12 @@
55
55
  "scripts": {
56
56
  "prepare": "node tools/install-hooks.mjs",
57
57
  "start": "node bin/serve.js",
58
- "build": "node build/bundle.mjs && tsc -p tsconfig.build.json && node -e \"import('./build/bundle.mjs').then(m=>m.marquerDistEsm())\"",
58
+ "build": "node -e \"require('fs').existsSync('build/bundle.mjs')||(console.error('Ce script ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && node build/bundle.mjs && tsc -p tsconfig.build.json && node -e \"import('./build/bundle.mjs').then(m=>m.marquerDistEsm())\"",
59
59
  "test": "node -e \"require('fs').existsSync('server/__tests__')||(console.error('Ce banc ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && vitest run",
60
60
  "test:watch": "vitest",
61
- "lint": "eslint bin context server src build",
61
+ "lint": "node -e \"require('fs').existsSync('eslint.config.mjs')||(console.error('Ce script ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && eslint bin context server src build",
62
62
  "lint:fix": "eslint bin context server src build --fix",
63
- "typecheck": "tsc --noEmit",
63
+ "typecheck": "node -e \"require('fs').existsSync('tsconfig.json')||(console.error('Ce script ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && tsc --noEmit",
64
64
  "prepublishOnly": "npm run build && npm test",
65
65
  "test:e2e": "node -e \"require('fs').existsSync('vitest.e2e.config.mjs')||(console.error('Ce banc ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && vitest run --config vitest.e2e.config.mjs",
66
66
  "test:base": "node -e \"require('fs').existsSync('vitest.base.config.mjs')||(console.error('Ce banc ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && vitest run --config vitest.base.config.mjs"
package/server/handler.js CHANGED
@@ -453,10 +453,21 @@ var Live=(function(){
453
453
  var t=document.getElementById('lModalT'),d=document.getElementById('lModalD'),y=document.getElementById('lModalYes'),n=document.getElementById('lModalNo');
454
454
  if(t)t.textContent=opts.title||'Confirmer ?';if(d){d.textContent=opts.desc||'';d.style.display=opts.desc?'block':'none';}
455
455
  if(y)y.textContent=opts.ok||'Confirmer';
456
- function close(){m.classList.remove('open');if(y)y.onclick=null;if(n)n.onclick=null;m.onclick=null;document.removeEventListener('keydown',key);}
457
- function key(e){if(e.key==='Escape'){close();}else if(e.key==='Enter'){close();if(onOk)onOk();}}
456
+ // ⚠️ UN role=dialog SANS PIÈGE DE FOCUS EST UNE DÉCLARATION SANS EFFET (septième audit) :
457
+ // Tab sortait vers la page derrière, et à la fermeture le focus tombait sur <body> — un
458
+ // utilisateur clavier repartait de zéro. Le piège boucle entre les DEUX boutons, Échap et
459
+ // Entrée existaient déjà, et l'élément qui avait le focus à l'ouverture le RETROUVE.
460
+ var avant=document.activeElement;
461
+ function close(){m.classList.remove('open');if(y)y.onclick=null;if(n)n.onclick=null;m.onclick=null;document.removeEventListener('keydown',key);
462
+ if(avant&&avant.focus)try{avant.focus();}catch(e){}}
463
+ function key(e){if(e.key==='Escape'){close();}else if(e.key==='Enter'){close();if(onOk)onOk();}
464
+ else if(e.key==='Tab'){var f=[n,y].filter(function(x){return x;});if(!f.length)return;e.preventDefault();
465
+ var i=f.indexOf(document.activeElement);var j=e.shiftKey?(i<=0?f.length-1:i-1):(i>=f.length-1?0:i+1);try{f[j].focus();}catch(err){}}}
458
466
  if(y)y.onclick=function(){close();if(onOk)onOk();};if(n)n.onclick=close;m.onclick=function(e){if(e.target===m)close();};
459
467
  document.addEventListener('keydown',key);m.classList.add('open');if(y)try{y.focus();}catch(e){}}
468
+ // Crochet interne (même famille que __presRelireEtat) : le piège de focus ne se PROUVE que
469
+ // piloté clavier dans un vrai navigateur, et la seule voie UI passe par un message à soi.
470
+ try{window.__confirmDialog=confirmDialog;}catch(e){}
460
471
  function history(){fetch('/api/doc?present='+encodeURIComponent(SLUG)+'&chat=1').then(function(r){return r.json();}).then(function(d){var box=document.getElementById('chatMsgs');if(d&&d.messages&&d.messages.length){d.messages.forEach(function(m){addMsg(m);});}else if(box&&!box.children.length){box.innerHTML='<div class=chat-empty>Aucun message. Lancez la discussion.</div>';}if(d&&typeof d.locked!=='undefined')applyLock(d.locked);_histDone=true;}).catch(function(){_histDone=true;});}
461
472
  function react(id,e){if(!ME||!id||!e)return;
462
473
  // ⚠️ ON ENVOIE L'ÉTAT VOULU, PAS « INVERSE ». Basculer n'a de sens qu'une fois : un renvoi
@@ -847,7 +858,7 @@ var Map3DD=(function(){
847
858
  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
859
  var tm=document.getElementById('mapToMap'); if(tm&&!tm._w){tm._w=1;tm.addEventListener('click',toMap);} }
849
860
  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){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>';}); }
861
+ 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)||la<-90||la>90||lo<-180||lo>180)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
862
  // ── Street View (Google) ─────────────────────────────────────────────────────────────────────────
852
863
  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
864
  // Le présentateur passe en Street View depuis le centre de la carte : on cherche le panorama le plus proche.
@@ -3613,10 +3624,21 @@ async function handler(req, res) {
3613
3624
  // demandes suivantes la trouveront par l'unicité, et les doublons d'avant 0011
3614
3625
  // s'éteignent d'eux-mêmes faute d'être resservis.
3615
3626
  const cleDispo = await require("./schema").attendue("liensUniques");
3616
- await PLAYER.db.request(`commercial_doc_shares?slug=eq.${encodeURIComponent(dejaLa[0].slug)}`, {
3617
- method: "PATCH", headers: { Prefer: "return=minimal" },
3618
- body: { doc_title: body.docTitle || null, file_url: String(body.fileUrl), file_name: body.fileName || null, revoked: false, ...(cleDispo ? { idem_key: cleHote } : {}) },
3619
- });
3627
+ // ⚠️ MÊME RATTRAPAGE QUE LE CHEMIN DE CRÉATION (septième audit : le correctif ne
3628
+ // s'était pas appliqué à lui-même). Deux doublons d'avant 0011 en concurrence :
3629
+ // l'index refuse la seconde pose de clé — on relit le gagnant au lieu de rendre 500.
3630
+ try {
3631
+ await PLAYER.db.request(`commercial_doc_shares?slug=eq.${encodeURIComponent(dejaLa[0].slug)}`, {
3632
+ method: "PATCH", headers: { Prefer: "return=minimal" },
3633
+ body: { doc_title: body.docTitle || null, file_url: String(body.fileUrl), file_name: body.fileName || null, revoked: false, ...(cleDispo ? { idem_key: cleHote } : {}) },
3634
+ });
3635
+ } catch (erreur) {
3636
+ if (!String((erreur && erreur.message) || "").includes("409")) throw erreur;
3637
+ try { PLAYER.errors.capture(new Error("backfill hôte : la clé était déjà posée ailleurs — " + docId), { route: "hostshare", benin: true }); } catch { /* jamais bloquant */ }
3638
+ const gagnant = await PLAYER.db.request(`commercial_doc_shares?idem_key=eq.${encodeURIComponent(cleHote)}&select=slug&limit=1`);
3639
+ if (!Array.isArray(gagnant) || !gagnant[0]) throw erreur;
3640
+ return jd(200, { ok: true, slug: gagnant[0].slug, reused: true });
3641
+ }
3620
3642
  return jd(200, { ok: true, slug: dejaLa[0].slug, reused: true });
3621
3643
  }
3622
3644
  // `createdBy` reste NUL : personne ne l'a créé. Le filtre « mes liens » compare
@@ -3684,7 +3706,16 @@ async function handler(req, res) {
3684
3706
  const cleTest = cleIdempotence("repetition", [docId]);
3685
3707
  if (Array.isArray(ex) && ex[0]) {
3686
3708
  const cleDispo = await require("./schema").attendue("liensUniques");
3687
- 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 } : {}) } });
3709
+ // Même rattrapage que ci-dessus : le backfill de la répétition pose la même clé.
3710
+ try {
3711
+ 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 } : {}) } });
3712
+ } catch (erreur) {
3713
+ if (!String((erreur && erreur.message) || "").includes("409")) throw erreur;
3714
+ try { PLAYER.errors.capture(new Error("backfill répétition : la clé était déjà posée ailleurs — " + docId), { route: "docshare-test", benin: true }); } catch { /* jamais bloquant */ }
3715
+ const gagnant = await PLAYER.db.request(`commercial_doc_shares?idem_key=eq.${encodeURIComponent(cleTest)}&select=slug&limit=1`);
3716
+ if (!Array.isArray(gagnant) || !gagnant[0]) throw erreur;
3717
+ return jd(200, { ok: true, slug: gagnant[0].slug, reused: true });
3718
+ }
3688
3719
  return jd(200, { ok: true, slug: ex[0].slug });
3689
3720
  }
3690
3721
  // Deux ouvertures simultanées de la répétition : la contrainte tranche, le perdant relit.
package/supabase/init.sql CHANGED
@@ -68,6 +68,12 @@ alter table public.commercial_doc_shares
68
68
  create unique index if not exists cds_idem_key_uniq
69
69
  on public.commercial_doc_shares (idem_key)
70
70
  where idem_key is not null;
71
+ comment on column public.commercial_doc_shares.idem_key is
72
+ 'Clé d''idempotence des liens SYSTÈME (hôte, répétition) : empreinte « genre:sha256(JSON des '
73
+ 'composants) » — genre = hote | repetition. Les clés historiques au format concaténé '
74
+ '« hote:<doc>|<email> » se remplacent d''elles-mêmes au réemploi (backfill). Nulle sur les '
75
+ 'liens ordinaires. Unique quand renseignée : deux demandes simultanées ne créent qu''un lien, '
76
+ 'la seconde relit le gagnant.';
71
77
  create index if not exists cds_parent_idx on public.commercial_doc_shares (parent_slug);
72
78
  create index if not exists idx_doc_shares_brand_key
73
79
  on public.commercial_doc_shares (brand_key) where brand_key is not null;
@@ -17,6 +17,9 @@
17
17
  alter table public.commercial_doc_shares
18
18
  add column if not exists idem_key text;
19
19
 
20
+ -- ⚠️ Le format décrit ci-dessous était celui de l'époque ; la clé est devenue une EMPREINTE
21
+ -- (genre:sha256) et la 0012 remet ce commentaire de colonne à jour EN BASE. Le texte d'origine
22
+ -- reste ici tel qu'appliqué — une migration ne se réécrit pas, elle se corrige par la suivante.
20
23
  comment on column public.commercial_doc_shares.idem_key is
21
24
  'Clé d''idempotence des liens SYSTÈME (hôte, répétition) : « hote:<doc>|<destinataire attesté> » '
22
25
  'ou « repetition:<doc> ». Nulle sur les liens ordinaires. Unique quand renseignée : deux '
@@ -0,0 +1,16 @@
1
+ -- LE COMMENTAIRE DE COLONNE DÉCRIVAIT UN FORMAT MORT.
2
+ --
3
+ -- La 0011 documentait la clé sous sa forme concaténée « hote:<doc>|<destinataire attesté> » ;
4
+ -- depuis, la clé est une EMPREINTE — genre:sha256(JSON.stringify(composants)) — parce que la
5
+ -- concaténation portait trois défauts (troncature à l'écriture mais relecture entière, collision
6
+ -- de préfixe, frontière « | » déplaçable — septième audit). Un commentaire de colonne se lit dans
7
+ -- la base, longtemps après les fichiers : celui-ci aurait envoyé un exploitant chercher des clés
8
+ -- lisibles qui n'existent plus.
9
+ --
10
+ -- Sans lui : rien ne casse — seule la documentation EN BASE reste fausse.
11
+ comment on column public.commercial_doc_shares.idem_key is
12
+ 'Clé d''idempotence des liens SYSTÈME (hôte, répétition) : empreinte « genre:sha256(JSON des '
13
+ 'composants) » — genre = hote | repetition. Les clés historiques au format concaténé '
14
+ '« hote:<doc>|<email> » se remplacent d''elles-mêmes au réemploi (backfill). Nulle sur les '
15
+ 'liens ordinaires. Unique quand renseignée : deux demandes simultanées ne créent qu''un lien, '
16
+ 'la seconde relit le gagnant.';