discovery-media-player 0.1.79 → 0.1.80
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/docs/HOST-CONTRACT.md +7 -0
- package/package.json +1 -1
- package/server/handler.js +6 -0
- package/server/retention.js +27 -25
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -35,6 +35,7 @@ need.
|
|
|
35
35
|
"frameAncestors": ["'self'", "https://*.vercel.app", "https://app.example.com"],
|
|
36
36
|
"separateIssuer": true,
|
|
37
37
|
"internalStrict": true,
|
|
38
|
+
"retentionSweep": false,
|
|
38
39
|
"hostShare": true,
|
|
39
40
|
"hostMail": true,
|
|
40
41
|
"plugins": { "bot": false, "visitors": false, "brandIntro": false, "botBrowser": false, "providerQuotas": false },
|
|
@@ -95,6 +96,12 @@ exactly the monitoring case where "no data" would otherwise read as "all clear".
|
|
|
95
96
|
⚠️ **`incomplet` wins over `partiel`**: a missing column is a positive fact and settles the verdict
|
|
96
97
|
on its own, even when the rest has not been checked.
|
|
97
98
|
|
|
99
|
+
⚠️ **`retentionSweep`** says whether the automatic retention purge is *armed*
|
|
100
|
+
(`config.retention.balayage === true`). The `retention` capability only says the instance *can*
|
|
101
|
+
purge; this boolean says whether it *does*, on its own, once a day. Default `false` — nothing is
|
|
102
|
+
deleted unless an operator wrote the policy. A cockpit can read this to know if an instance is
|
|
103
|
+
subject to automatic deletion, instead of inferring it from a log.
|
|
104
|
+
|
|
98
105
|
⚠️ **`internalStrict: false` means internal identity comes from the browser.** In transitional
|
|
99
106
|
mode the internal-analytics route accepts `docId`, `email` and `name` as the client declares them —
|
|
100
107
|
a caller can fabricate "this colleague read this document". Strict mode (`PLAYER_INTERNAL_STRICT=1`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.80",
|
|
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
|
@@ -595,6 +595,12 @@ async function handler(req, res) {
|
|
|
595
595
|
// MESURABLE : un cockpit peut refuser une instance non stricte, au lieu de le découvrir en
|
|
596
596
|
// lisant un journal. Demandé par le second hôte (cinquième audit, P1-4).
|
|
597
597
|
internalStrict: !!(PLAYER.config && PLAYER.config.internalStrict),
|
|
598
|
+
// ⚠️ LE BALAYAGE DE RÉTENTION EST-IL ARMÉ ? La capacité `retention` dit que l'instance PEUT
|
|
599
|
+
// purger ; ce booléen dit si le balayage automatique TOURNE (`config.retention.balayage`).
|
|
600
|
+
// Sans lui, une instance armée est indiscernable d'une instance éteinte — et une purge qui
|
|
601
|
+
// supprime se compose mal avec l'ignorance de savoir si l'on est concerné (signalé par le
|
|
602
|
+
// second hôte). Défaut false, comme la décision par défaut : rien ne s'efface tout seul.
|
|
603
|
+
retentionSweep: !!(PLAYER.config && PLAYER.config.retention && PLAYER.config.retention.balayage === true),
|
|
598
604
|
// ⚠️ L'ÉTAT DU SCHÉMA, LÀ OÙ ON REGARDE. Une colonne absente était signalée par un
|
|
599
605
|
// `console.warn`, une fois par processus : sur une fonction serverless, une ligne perdue
|
|
600
606
|
// dans une sortie que personne n'ouvre tant que tout a l'air de marcher — et « tout a
|
package/server/retention.js
CHANGED
|
@@ -90,6 +90,24 @@ function optionsValidees(opts) {
|
|
|
90
90
|
// débit, par exemple). Guillemeter TOUJOURS est correct et évite d'avoir à deviner.
|
|
91
91
|
const guill = (v) => '"' + String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
92
92
|
|
|
93
|
+
// ⚠️ LES DEUX SEULES PORTES DE DESTRUCTION DU MODULE — dryRun court-circuité en PREMIÈRE ligne.
|
|
94
|
+
// « Honoré » ne se prouve pas en lisant chaque DELETE (une fenêtre de lecture décide de la réponse
|
|
95
|
+
// — le piège du périmètre), mais en n'ayant QU'UNE porte, gardée ici, et une garde de forme qui
|
|
96
|
+
// exige qu'il n'y en ait qu'une (server/__tests__/retentionUnePorte.test.js). Un troisième chemin
|
|
97
|
+
// d'écriture DEVRA passer par ces portes, ou il rougira le compte. C'est « retirer la seconde
|
|
98
|
+
// source de vérité » appliqué à la suppression : un seul endroit peut détruire.
|
|
99
|
+
async function effacerParIds(table, colId, ids, opts) {
|
|
100
|
+
if (opts.dryRun || !ids || !ids.length) return 0;
|
|
101
|
+
const del = await PLAYER.db.request(`${table}?${colId}=in.(${ids.map(guill).join(",")})&select=${colId}`, { method: "DELETE", headers: { Prefer: "return=representation" } });
|
|
102
|
+
return Array.isArray(del) ? del.length : 0; // lignes RENDUES, pas ids présélectionnés
|
|
103
|
+
}
|
|
104
|
+
async function retirerFichier(chemin, opts) {
|
|
105
|
+
if (opts.dryRun || !chemin) return null; // null = rien tenté ; true = retiré ; false = échec
|
|
106
|
+
const retirer = PLAYER.storage && typeof PLAYER.storage.remove === "function" ? PLAYER.storage.remove.bind(PLAYER.storage) : null;
|
|
107
|
+
if (!retirer) return null;
|
|
108
|
+
try { return !!(await retirer("present-attachments", chemin)); } catch { return false; }
|
|
109
|
+
}
|
|
110
|
+
|
|
93
111
|
// ⚠️ PURGE PAR LOTS BORNÉS (P2 huitième audit). On SÉLECTIONNE un lot d'identifiants (borné,
|
|
94
112
|
// ordonné par la colonne de date), on les supprime par `id=in.(…)`, on recommence — jamais un
|
|
95
113
|
// DELETE non borné qui ramènerait tout l'historique d'un coup (mémoire, WAL, verrous, timeout).
|
|
@@ -113,13 +131,7 @@ async function purgerParLots(table, filtre, colId, { dryRun = false, taille = LO
|
|
|
113
131
|
if (!Array.isArray(lot) || !lot.length) break;
|
|
114
132
|
examinees += lot.length;
|
|
115
133
|
curseur = lot[lot.length - 1][colId];
|
|
116
|
-
|
|
117
|
-
const ids = lot.map((r) => r[colId]).filter((v) => v != null).map(guill);
|
|
118
|
-
if (ids.length) {
|
|
119
|
-
const del = await PLAYER.db.request(`${table}?${colId}=in.(${ids.join(",")})&select=${colId}`, { method: "DELETE", headers: { Prefer: "return=representation" } });
|
|
120
|
-
supprimees += Array.isArray(del) ? del.length : 0;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
134
|
+
supprimees += await effacerParIds(table, colId, lot.map((r) => r[colId]).filter((v) => v != null), { dryRun });
|
|
123
135
|
if (lot.length < limite) break; // dernier lot (moins que demandé → plus rien après)
|
|
124
136
|
}
|
|
125
137
|
return { examinees, supprimees, tronque };
|
|
@@ -142,7 +154,7 @@ const { cheminPieceJointe: cheminSurSlug } = require("./presentations");
|
|
|
142
154
|
// Purge des messages d'une présentation morte, par lots bornés qui lisent id+attachment ENSEMBLE :
|
|
143
155
|
// on retire les fichiers du bucket du lot (si l'hôte sait), puis on supprime le lot. Rend `tronque`
|
|
144
156
|
// pour que l'appelant décide de garder ou non la présentation. Compte les lignes RENDUES.
|
|
145
|
-
async function purgerMessagesPresentation(slug, opts,
|
|
157
|
+
async function purgerMessagesPresentation(slug, opts, base) {
|
|
146
158
|
const { dryRun, taille, plafond } = opts;
|
|
147
159
|
let supprimees = 0, fichiers = 0, fichiersErreur = 0, examinees = 0, tronque = false, curseur = null;
|
|
148
160
|
for (;;) {
|
|
@@ -154,22 +166,13 @@ async function purgerMessagesPresentation(slug, opts, retirer, base) {
|
|
|
154
166
|
if (!Array.isArray(lot) || !lot.length) break;
|
|
155
167
|
examinees += lot.length;
|
|
156
168
|
curseur = lot[lot.length - 1].id;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!chemin) continue; // hors du dossier du slug → jamais supprimé (barrière 2)
|
|
163
|
-
try { if (await retirer("present-attachments", chemin)) fichiers += 1; else fichiersErreur += 1; } // false = échec compté aussi
|
|
164
|
-
catch { fichiersErreur += 1; }
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const ids = lot.map((r) => r.id).filter((v) => v != null).map(guill);
|
|
168
|
-
if (ids.length) {
|
|
169
|
-
const del = await PLAYER.db.request(`doc_presentation_messages?id=in.(${ids.join(",")})&select=id`, { method: "DELETE", headers: { Prefer: "return=representation" } });
|
|
170
|
-
supprimees += Array.isArray(del) ? del.length : 0;
|
|
171
|
-
}
|
|
169
|
+
for (const j of lot) {
|
|
170
|
+
const url = j.attachment && (typeof j.attachment === "object" ? j.attachment.url : j.attachment);
|
|
171
|
+
const chemin = cheminSurSlug(url, slug, base); // hors du dossier du slug → null → jamais retiré (barrière 2)
|
|
172
|
+
const issue = await retirerFichier(chemin, { dryRun });
|
|
173
|
+
if (issue === true) fichiers += 1; else if (issue === false) fichiersErreur += 1; // false = échec compté
|
|
172
174
|
}
|
|
175
|
+
supprimees += await effacerParIds("doc_presentation_messages", "id", lot.map((r) => r.id).filter((v) => v != null), { dryRun });
|
|
173
176
|
if (lot.length < limite) break;
|
|
174
177
|
}
|
|
175
178
|
return { supprimees, fichiers, fichiersErreur, examinees, tronque };
|
|
@@ -204,13 +207,12 @@ async function purgerRetention(now, optsBrutes = {}) {
|
|
|
204
207
|
const bPres = borne(now, f.presentationsMois);
|
|
205
208
|
const mortes = await PLAYER.db.request(`doc_presentations?active=eq.false&updated_at=lt.${enc(bPres)}&select=slug&order=slug.asc&limit=${PLAFOND_PRESENTATIONS}`);
|
|
206
209
|
const presRapport = { examinees: 0, supprimees: 0, messages: 0, presences: 0, fichiers: 0, fichiersErreur: 0, tronque: Array.isArray(mortes) && mortes.length >= PLAFOND_PRESENTATIONS };
|
|
207
|
-
const retirer = PLAYER.storage && typeof PLAYER.storage.remove === "function" ? PLAYER.storage.remove.bind(PLAYER.storage) : null;
|
|
208
210
|
for (const p of (Array.isArray(mortes) ? mortes : [])) {
|
|
209
211
|
const slug = p && p.slug; if (!slug) continue;
|
|
210
212
|
presRapport.examinees += 1;
|
|
211
213
|
// Messages : lot borné qui lit id ET attachment ENSEMBLE (la lecture des pièces jointes n'est
|
|
212
214
|
// plus une requête globale non bornée — P2), retire les fichiers du lot, puis supprime le lot.
|
|
213
|
-
const msgs = await purgerMessagesPresentation(slug, opts,
|
|
215
|
+
const msgs = await purgerMessagesPresentation(slug, opts, base);
|
|
214
216
|
presRapport.messages += msgs.supprimees;
|
|
215
217
|
presRapport.fichiers += msgs.fichiers;
|
|
216
218
|
presRapport.fichiersErreur += msgs.fichiersErreur;
|