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 +1 -1
- package/server/handler.js +11 -6
- package/server/shares.js +14 -2
- package/supabase/init.sql +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
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="'+
|
|
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,'<')+'</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 =
|
|
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 =
|
|
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
|
-
|
|
3928
|
-
|
|
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)
|
|
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 = {
|
|
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;
|