discovery-media-player 0.1.147 → 0.1.148

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.
@@ -482,6 +482,27 @@ forwarded re-shares of their own links included, because they caused those readi
482
482
  changes for a role you already answer *yes* to. If your table predates `list.all`, see the warning
483
483
  above: an unheard-of action answered *no* narrows this view rather than breaking it.
484
484
 
485
+ ⚠️ **`?contract=1&schema=1` now tells you what is still stored, not only what you may purge.**
486
+ `retentionSweep` says the instance *can* purge; it says nothing about what has piled up. The card
487
+ gains a `purge` block counting the rows that still carry a reader IP or a raw User-Agent:
488
+
489
+ "purge": { "borne": 1000, "sessionsIp": 0, "sessionsUa": 0, "vuesUa": 0, "vide": true }
490
+
491
+ `vide` is the reading that matters: `true` means nothing of that legacy is left **on this
492
+ instance's live rows** — the condition under which those columns can eventually be dropped —
493
+ `false` means rows remain, and **`null` means at least one probe did not answer**. A count is
494
+ `null` for the same reason: a failed probe must never read as a zero, because zero is the answer
495
+ that authorises a deletion.
496
+
497
+ The counts are **bounded** at `borne` rows and read one small column, so `1000` reads as *at least
498
+ a thousand*, never as exactly. They run only under `&schema=1`, the mode where you have asked for
499
+ the database.
500
+
501
+ ⚠️ **Why this exists at all:** our tables live in *your* database, and your audit enumerates *your*
502
+ tables — a dependency's schema occupies a zone nobody's inventory visits. Two integrating hosts
503
+ found 2361 rows still carrying these columns, and they found them because a third party asked a
504
+ question about its own database, not because anything told them.
505
+
485
506
  ⚠️ **The reader IP is erased, and a direct query of your own will start seeing nothing.** The
486
507
  sessions table carried `ip` in the clear. `0.1.147` stops serving it — no player path reads it back,
487
508
  so nothing in this contract changes — stops writing it, and ships migration **0026**, which erases
package/docs/RETENTION.md CHANGED
@@ -177,11 +177,35 @@ end on a populated database: 200 rows kept, 200 addresses gone after a routine v
177
177
  replayable with no further effect. **The erasure is therefore complete today.** What is deferred is
178
178
  the shape of the schema, not the data.
179
179
 
180
- **When the columns are removed, and how to know the moment has come.** Not until **every deployed
181
- host runs a version that no longer writes them** the release carrying 0026 and 0027, or later.
182
- Until then a `DROP` of any of the three would fail every session and view write of a host that
183
- applies migrations before deploying. The check is not a date: it is whether the oldest player version
184
- still in service is at or past that release.
180
+ **Dropping the three columns is YOUR decision, not a migration we will ship.** This section used to
181
+ say the removal would come "in a later release", which was misleading: **we cannot know which player
182
+ version runs against your database, and you can.** A `DROP` is only safe once every instance writing
183
+ to that database is on `0.1.147` or later; on `0.1.145` and earlier PostgREST would reject every
184
+ session and view write, with an error naming a column rather than a version. Shipping that `DROP` in
185
+ `supabase/migrations/`, which every host replays, would hand the same irreversible gesture to hosts
186
+ whose deployment we have never seen. So it stays where the answer is known — with you.
187
+
188
+ **How to know the moment has come.** `?contract=1&schema=1` reports `purge.vide`. When it is `true`
189
+ on every instance pointing at that database, and every one of them is on `0.1.147` or later, nothing
190
+ writes those columns any more. Then, if you want the schema tidied:
191
+
192
+ ```sql
193
+ alter table public.commercial_doc_sessions drop column if exists ip;
194
+ alter table public.commercial_doc_sessions drop column if exists ua;
195
+ alter table public.commercial_doc_views drop column if exists ua;
196
+ ```
197
+
198
+ ⚠️ **This buys tidiness, not erasure — the erasure already happened.** `0026` and `0027` are what
199
+ removed the values, and routine autovacuum is what removed them from the pages (measured on a real
200
+ host: four seconds after the second migration, no lock, nothing triggered by hand). A `DROP` on
201
+ already-empty columns rewrites nothing and frees nothing. Run it because a schema should say what it
202
+ holds, not because anything is still there.
203
+
204
+ ⚠️ **And you lose the attestation with the column.** The comment carried by each column — readable
205
+ through `col_description()` — is what proves the purge was applied; a count of zero does not, since
206
+ it cannot tell "purged" from "never written". Capture that proof before dropping if you may need to
207
+ show it. After the drop, `purge.vide` still reads `true`: an absent column is a known state, not an
208
+ unknown one.
185
209
 
186
210
  **Why the column itself survives, for now.** A migration here must be safe to apply *while the
187
211
  previous version of the player is running* — that rule is what makes the deployment order harmless,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.147",
3
+ "version": "0.1.148",
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
@@ -801,6 +801,18 @@ async function handlerMesure(req, res) {
801
801
  ...(String(q.schema || "") === "1"
802
802
  ? await require("./schema").sonderTout()
803
803
  : require("./schema").etatDuSchema()) },
804
+ // ⚠️ CE QUI S'ACCUMULE, PAS SEULEMENT CE QU'ON PEUT PURGER. `retentionSweep` ci-dessus dit
805
+ // « je PEUX purger » ; il ne dit rien de ce qui est là. Nos tables vivent dans la base de
806
+ // l'hôte, et l'audit d'un hôte énumère SES tables : le schéma d'une dépendance occupe une
807
+ // zone que les inventaires de personne ne visitent. Deux hôtes ont trouvé 2361 lignes
808
+ // portant encore une adresse ou un agent brut — parce qu'un TIERS avait posé une question
809
+ // sur SA base, pas parce que quoi que ce soit le leur avait dit.
810
+ //
811
+ // Sous `&schema=1` seulement : c'est une lecture de la base, et sans le paramètre cette
812
+ // carte garde sa propriété de répondre quand plus rien ne répond.
813
+ ...(String(q.schema || "") === "1"
814
+ ? { purge: await require("./retention").resteDeLaPurge() }
815
+ : {}),
804
816
  // « L'hôte peut-il créer un lien en son nom propre ? » — configuré, pas seulement
805
817
  // possible. Un hôte qui oublie le secret reçoit un 401 qui ressemble à un droit
806
818
  // manquant ; ce booléen le lui dit sans qu'il ait à essayer.
@@ -391,4 +391,80 @@ function tick() {
391
391
  .catch((e) => { try { PLAYER.errors.capture(e, { route: "retention", benin: true }); } catch { /* jamais bloquant */ } });
392
392
  }
393
393
 
394
- module.exports = { init, purgerRetention, tick, borne };
394
+ /**
395
+ * CE QUI RESTE DE L'HÉRITAGE, CHEZ CET HÔTE — les lignes qui portent encore une adresse IP ou un
396
+ * User-Agent brut.
397
+ *
398
+ * ⚠️ POURQUOI CE COMPTEUR EXISTE, ET C'EST UN HÔTE QUI L'A DIT. Nos tables vivent dans la base de
399
+ * nos hôtes, et l'audit d'un hôte énumère SES tables : le schéma d'une dépendance occupe une zone
400
+ * que les inventaires de personne ne visitent. Deux hôtes ont découvert 2361 lignes portant ces
401
+ * colonnes — non pas en surveillant, mais parce qu'un TIERS avait posé une question sur SA base.
402
+ * `retentionSweep` dit « je PEUX purger » ; il ne dit pas CE QUI S'ACCUMULE. Ce compteur le dit,
403
+ * chez chacun, sans que personne ait à y penser.
404
+ *
405
+ * ⚠️ ET IL RÉPOND À LA QUESTION QUI DÉCIDE DU RETRAIT DES COLONNES. `0026` et `0027` VIDENT sans
406
+ * supprimer, parce qu'une migration doit rester sûre pendant que la version précédente du code
407
+ * tourne. Le retrait attend que plus aucune version supportée ne les écrive — une condition qu'on
408
+ * ne peut aujourd'hui que SUPPOSER, en croyant savoir quelle version tourne chez qui. `vide` la
409
+ * rend LISIBLE.
410
+ *
411
+ * ⚠️ ON COMPTE DES LIGNES, PAS UN `count=exact`. La capacité `db` de l'hôte rend le corps de la
412
+ * réponse, pas ses en-têtes : le compte de PostgREST voyage dans `Content-Range`, donc il serait
413
+ * illisible sans élargir le contrat d'hôte — ce qu'un compteur de diagnostic ne justifie pas.
414
+ * D'où un comptage BORNÉ : au plus `BORNE_RESTE` identifiants, une seule petite colonne. Atteindre
415
+ * la borne se lit « au moins autant », jamais « exactement ».
416
+ *
417
+ * ⚠️ ET LE COÛT EST INVERSE DE L'INTUITION, donc il est dit plutôt que caché : quand il reste
418
+ * beaucoup de lignes, la base s'arrête à la borne et c'est rapide ; quand il n'en reste AUCUNE,
419
+ * elle parcourt la table pour ne rien trouver. Le cas cher est le cas terminal — celui où ce
420
+ * compteur a fini son office et disparaîtra avec les colonnes qu'il surveille. Il ne s'exécute
421
+ * d'ailleurs que sur `?contract=1&schema=1`, le seul mode où l'appelant demande la base.
422
+ *
423
+ * ⚠️ UN ÉCHEC REND `null`, JAMAIS ZÉRO. Zéro est la réponse qui autorise à supprimer une colonne :
424
+ * la fabriquer à partir d'une sonde en panne serait le pire mensonge que cette carte puisse faire.
425
+ */
426
+ const BORNE_RESTE = 1000;
427
+
428
+ const SONDES_RESTE = [
429
+ ["sessionsIp", "commercial_doc_sessions", "session_id", "ip"],
430
+ ["sessionsUa", "commercial_doc_sessions", "session_id", "ua"],
431
+ ["vuesUa", "commercial_doc_views", "id", "ua"],
432
+ ];
433
+
434
+ /**
435
+ * ⚠️ ET LA COLONNE DISPARUE EST UN ÉTAT CONNU, PAS UNE PANNE. Le jour où un exploitant supprime ces
436
+ * colonnes — le geste que ce compteur sert à autoriser — la requête échoue avec le
437
+ * `42703` de PostgreSQL, « colonne inexistante ». Rendre `null` ferait alors lire « on ne sait
438
+ * pas » au moment EXACT où l'on sait le mieux : plus rien ne peut porter une colonne qui n'existe
439
+ * plus. Le compteur deviendrait aveugle précisément quand son sujet est réglé.
440
+ *
441
+ * Toute autre erreur reste `null`. Et un hôte dont la capacité `db` ne rend pas le corps analysé
442
+ * retombe sur `null` : ne pas savoir est le côté sûr, puisque zéro est ce qui autorise à supprimer.
443
+ */
444
+ const COLONNE_ABSENTE = "42703";
445
+
446
+ async function compterReste(table, cle, colonne) {
447
+ try {
448
+ const lignes = await PLAYER.db.request(
449
+ `${table}?select=${cle}&${colonne}=not.is.null&limit=${BORNE_RESTE}`,
450
+ { timeoutMs: 8000 },
451
+ );
452
+ return Array.isArray(lignes) ? lignes.length : null;
453
+ } catch (e) {
454
+ if (e && e.details && e.details.code === COLONNE_ABSENTE) return 0;
455
+ return null; // indéterminé — surtout pas zéro
456
+ }
457
+ }
458
+
459
+ async function resteDeLaPurge() {
460
+ const comptes = await Promise.all(SONDES_RESTE.map(([, t, c, col]) => compterReste(t, c, col)));
461
+ const out = { borne: BORNE_RESTE };
462
+ SONDES_RESTE.forEach(([nom], i) => { out[nom] = comptes[i]; });
463
+ // ⚠️ TROIS ÉTATS, PAS DEUX. `true` : plus rien, le retrait des colonnes est permis ICI. `false` :
464
+ // il reste des lignes. `null` : au moins une sonde n'a pas répondu — on ne sait pas, et « on ne
465
+ // sait pas » ne doit jamais se lire comme « c'est bon ».
466
+ out.vide = comptes.some((n) => n === null) ? null : comptes.every((n) => n === 0);
467
+ return out;
468
+ }
469
+
470
+ module.exports = { init, purgerRetention, tick, borne, resteDeLaPurge, BORNE_RESTE };