discovery-media-player 0.1.165 → 0.1.166
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/context/standalone.js +8 -3
- package/docs/HOST-CONTRACT.md +30 -0
- package/package.json +1 -1
- package/server/bornes.js +59 -0
- package/server/handler.js +52 -18
- package/server/routes-visiteur.js +5 -3
- package/types/context.d.ts +8 -4
package/context/standalone.js
CHANGED
|
@@ -842,11 +842,16 @@ function createStandaloneContext(env = process.env) {
|
|
|
842
842
|
extraFrameAncestors: String(env.DOC_FRAME_ANCESTORS || "").split(/\s+/).filter(Boolean),
|
|
843
843
|
// Transferts de fichiers simultanés par processus (défaut 64) : le relais refuse en 503 au-delà,
|
|
844
844
|
// avant tout appel amont. Lu ICI, pas dans le cœur — la configuration entre par le contexte.
|
|
845
|
-
|
|
845
|
+
// ⚠️ TRANSMIS TEL QUEL, PAS NORMALISÉ ICI. La première écriture faisait `Number(x) || 64` : une
|
|
846
|
+
// valeur illisible devenait le défaut EN SILENCE, et 2 147 483 648 passait jusqu'à `setTimeout`,
|
|
847
|
+
// qui ramène un tel délai à 1 ms (audit externe, cinquième passe). Les plages vivent dans
|
|
848
|
+
// `server/bornes.js`, et le cœur les applique à `init` en DISANT une fois ce qu'il a refusé —
|
|
849
|
+
// si on bornait ici, il ne verrait qu'une valeur valide et l'exploitant ne saurait jamais.
|
|
850
|
+
maxConcurrentRelays: env.PLAYER_MAX_RELAYS ? Number(env.PLAYER_MAX_RELAYS) : 64,
|
|
846
851
|
// Un relais sans progression pendant relayStallMs, ou plus long que relayMaxMs, est abandonné
|
|
847
852
|
// (source et réponse détruites) : sans ça, un client qui cesse de lire garde sa place pour toujours.
|
|
848
|
-
relayStallMs: Number(env.PLAYER_RELAY_STALL_MS
|
|
849
|
-
relayMaxMs: Number(env.PLAYER_RELAY_MAX_MS
|
|
853
|
+
relayStallMs: env.PLAYER_RELAY_STALL_MS ? Number(env.PLAYER_RELAY_STALL_MS) : 30_000,
|
|
854
|
+
relayMaxMs: env.PLAYER_RELAY_MAX_MS ? Number(env.PLAYER_RELAY_MAX_MS) : 900_000,
|
|
850
855
|
|
|
851
856
|
/**
|
|
852
857
|
* Clé de `localStorage` sous laquelle VOTRE application range la session de ses membres.
|
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -50,6 +50,7 @@ need.
|
|
|
50
50
|
"presenceDurcissement": "inconnu",
|
|
51
51
|
"presenceFusion": "inconnu",
|
|
52
52
|
"lectureSaturee": { "total": 0, "fenetreS": 0, "derniereIlYaS": null },
|
|
53
|
+
"relaisRefuses": { "total": 0, "fenetreS": 0, "derniereIlYaS": null },
|
|
53
54
|
"mesures": { "fenetreS": 0, "seauxMs": [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], "familles": ["document", "presentation", "action", "fichier", "carte", "autre"], "routes": {}, "base": { "n": 0 }, "statuts": { "ok": 0, "refus4xx": 0, "debit429": 0, "occupe503": 0, "erreur5xx": 0 }, "memoireMio": { "rss": 0, "heap": 0, "tampons": 0 }, "boucleMs": { "n": 0, "moyen": null, "p99": null, "resolutionMs": 20 } },
|
|
54
55
|
"retentionSweep": false,
|
|
55
56
|
"hostShare": true,
|
|
@@ -241,6 +242,18 @@ object rather than as separate fields you could read apart.
|
|
|
241
242
|
not of your deployment. Aggregating is your job — and letting you believe otherwise would be worse
|
|
242
243
|
than returning nothing.
|
|
243
244
|
|
|
245
|
+
### `relaisRefuses` — what the relay admission refused
|
|
246
|
+
|
|
247
|
+
Same three keys, same reading rules, **another ceiling**: this counts the file relays refused with
|
|
248
|
+
`503` + `Retry-After: 2` because `config.maxConcurrentRelays` was reached (see *Relay `Range`* in the
|
|
249
|
+
three things a host implements). ⚠️ `lectureSaturee` does **not** cover it — it is the read cache
|
|
250
|
+
only. A host answered *"we never saturate relays"* from `lectureSaturee.total = 0` (13/09), which was
|
|
251
|
+
a reasonable reading of a card that had no relay counter; and `mesures.statuts.occupe503` mixes the
|
|
252
|
+
two refusals. This field exists so that the question *did this instance refuse a relay?* is answered
|
|
253
|
+
by the card, structured and dated, and never by a search through logs for `relais refusés` — the
|
|
254
|
+
log line stays for diagnosis, the card is the way to *notice*. Process-local like everything on this
|
|
255
|
+
card, and **never reset by `init`**, exactly like the counter of open relays.
|
|
256
|
+
|
|
244
257
|
⚠️ **Before you upgrade, do not read `presenceDurcissement` or `presenceFusion`.** They are *reports
|
|
245
258
|
of execution*: on an instance where nothing is running they say `inconnu`, which means *nobody
|
|
246
259
|
looked* — not *the migration is there*. A pre-flight check built on one of them silently passes on
|
|
@@ -771,6 +784,15 @@ Four requirements, in order of what they cost when missed:
|
|
|
771
784
|
`config.relayStallMs` (30 s) or a total beyond `config.relayMaxMs` (15 min) aborts the pipeline,
|
|
772
785
|
destroying your response and the client's. A client that stops reading no longer keeps a slot
|
|
773
786
|
forever; a route of yours that stops sending does not either.
|
|
787
|
+
⚠️ Three things about those numbers, all measured on 0.1.165 by an audit: **64 is not a safe
|
|
788
|
+
default everywhere** — 64 relays × 8 MiB with slow consumers took the process RSS from ~63 MiB to
|
|
789
|
+
193–257 MiB; on a 256 MiB process set 16–32. The settings are **integers in a written range**
|
|
790
|
+
(`maxConcurrentRelays` 1–1024, the two delays 1–86 400 000 ms): out of range falls back to the
|
|
791
|
+
default and is reported once at `init` through `errors.capture`, because `setTimeout` clamps
|
|
792
|
+
anything above 2 147 483 647 ms to 1 ms and a "24-day" delay aborted a relay in 6 ms. And the
|
|
793
|
+
**counter of open relays is never reset by `init`**: calling `init` again re-reads the ceiling and
|
|
794
|
+
the delays for the relays admitted afterwards; one already in flight keeps its slot and its
|
|
795
|
+
bounds, so a re-initialisation cannot let a request past a full ceiling.
|
|
774
796
|
3. **Accept a server-to-server call.** A tracked link is opened by someone with no session on your
|
|
775
797
|
side. Authenticate the player with the shared secret in the `x-player-fetch-secret` **header** —
|
|
776
798
|
header only, never a query string: logs keep URLs.
|
|
@@ -821,6 +843,14 @@ back on an inability to *reach*.** And "do not fall back" applies to what you **
|
|
|
821
843
|
|
|
822
844
|
## What will bite
|
|
823
845
|
|
|
846
|
+
⚠️ **`node_modules` cannot be a symlink, and a repository inside a syncing folder will break under
|
|
847
|
+
you.** Reported by a host (13/09), not reproduced here: a checkout living in iCloud Drive had files
|
|
848
|
+
duplicated and emptied *while being worked on* — four breakages in six days, four different
|
|
849
|
+
signatures, one of which blocked a delivery. Their escape route, a symbolic link from `node_modules`
|
|
850
|
+
to a folder outside the sync, is a dead end: `npm` replaces the link with a real directory, whether
|
|
851
|
+
the target is empty or populated, on `install` as on `ci`. Keep the clone itself outside any
|
|
852
|
+
synchronised folder; there is no way to keep only its dependencies out.
|
|
853
|
+
|
|
824
854
|
⚠️ **Every capability you provide must settle in bounded time — `db.request` first of all.** The
|
|
825
855
|
player awaits your `db.request`, `storage.fetchFile`, `mail.send` and the visitor plugin; a promise
|
|
826
856
|
that never settles keeps a request in flight, and the read cache admits at most 128 in-flight reads
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.166",
|
|
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/bornes.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright © 2026 3D Discovery
|
|
3
|
+
// LES RÉGLAGES DU RELAIS, BORNÉS À UN SEUL ENDROIT — parce qu'un nombre fini n'est pas un délai.
|
|
4
|
+
//
|
|
5
|
+
// ⚠️ `setTimeout` PLAFONNE À 2 147 483 647 ms ET RAMÈNE TOUT DÉPASSEMENT À 1 ms. Un délai de
|
|
6
|
+
// progression configuré à 2 147 483 648 ms — « environ 24,8 jours » — abandonnait le transfert en
|
|
7
|
+
// 6 ms, avec 65 `TimeoutOverflowWarning` pour le dire (reproduit par un audit externe, cinquième
|
|
8
|
+
// passe, 13/09). La première borne acceptait « tout nombre fini positif » : une décimale, l'infini
|
|
9
|
+
// sous la forme d'un très grand entier, tout passait jusqu'à Node. On n'accepte donc qu'un ENTIER
|
|
10
|
+
// SÛR dans une plage écrite, bien sous la limite native, et une valeur hors plage retombe sur le
|
|
11
|
+
// défaut EN LE DISANT — une fois, à l'initialisation, pas à chaque relais.
|
|
12
|
+
//
|
|
13
|
+
// Le même module sert le cœur (`handler.init`) et le contexte autonome : une seule décision.
|
|
14
|
+
|
|
15
|
+
/** Relais simultanés par processus. 1 024 est une borne de configuration, pas une recommandation :
|
|
16
|
+
* 64 relais × 8 Mio avec des clients lents ont fait monter la RSS de 63 à 193–257 Mio (mesuré par
|
|
17
|
+
* un audit externe) — sur un processus à 256 Mio, 16 à 32 est le bon ordre de grandeur. */
|
|
18
|
+
const RELAIS_SIMULTANES_DEFAUT = 64, RELAIS_SIMULTANES_MIN = 1, RELAIS_SIMULTANES_MAX = 1024;
|
|
19
|
+
/** Délais du relais, en millisecondes entières : de 1 ms à 24 h. Vingt-quatre heures est une borne
|
|
20
|
+
* applicative, très en dessous des 2 147 483 647 ms que `setTimeout` sait tenir. */
|
|
21
|
+
const RELAIS_STALL_MS_DEFAUT = 30_000, RELAIS_MAX_MS_DEFAUT = 900_000;
|
|
22
|
+
const RELAIS_MS_MIN = 1, RELAIS_MS_MAX = 86_400_000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Un entier sûr dans [min, max], sinon le défaut. `undefined` et `null` sont « non posé » : le
|
|
26
|
+
* défaut s'applique sans être un défaut de configuration ; tout le reste hors plage l'est.
|
|
27
|
+
* @returns {{ valeur: number, valide: boolean, posee: boolean }}
|
|
28
|
+
*/
|
|
29
|
+
function entierBorne(v, { defaut, min, max }) {
|
|
30
|
+
if (v === undefined || v === null || v === "") return { valeur: defaut, valide: true, posee: false };
|
|
31
|
+
// Un nombre, ou une chaîne (l'environnement n'en connaît pas d'autre) — jamais un booléen, que
|
|
32
|
+
// `Number(true)` transformerait en un délai de 1 ms parfaitement « valide ».
|
|
33
|
+
const n = typeof v === "number" ? v : typeof v === "string" ? Number(v.trim()) : NaN;
|
|
34
|
+
const valide = Number.isSafeInteger(n) && n >= min && n <= max;
|
|
35
|
+
return { valeur: valide ? n : defaut, valide, posee: true };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Les trois réglages du relais, lus dans `config`, bornés, avec la liste de ce qui a été refusé —
|
|
40
|
+
* pour que l'appelant le dise une fois, avec la plage, plutôt que d'appliquer un défaut en silence.
|
|
41
|
+
* @returns {{ plafond: number, stallMs: number, maxMs: number, invalides: string[] }}
|
|
42
|
+
*/
|
|
43
|
+
function bornesRelais(config) {
|
|
44
|
+
const c = config || {};
|
|
45
|
+
const plafond = entierBorne(c.maxConcurrentRelays, { defaut: RELAIS_SIMULTANES_DEFAUT, min: RELAIS_SIMULTANES_MIN, max: RELAIS_SIMULTANES_MAX });
|
|
46
|
+
const stall = entierBorne(c.relayStallMs, { defaut: RELAIS_STALL_MS_DEFAUT, min: RELAIS_MS_MIN, max: RELAIS_MS_MAX });
|
|
47
|
+
const total = entierBorne(c.relayMaxMs, { defaut: RELAIS_MAX_MS_DEFAUT, min: RELAIS_MS_MIN, max: RELAIS_MS_MAX });
|
|
48
|
+
const invalides = [];
|
|
49
|
+
if (!plafond.valide) invalides.push(`maxConcurrentRelays=${String(c.maxConcurrentRelays)} (entier de ${RELAIS_SIMULTANES_MIN} à ${RELAIS_SIMULTANES_MAX}, défaut ${RELAIS_SIMULTANES_DEFAUT})`);
|
|
50
|
+
if (!stall.valide) invalides.push(`relayStallMs=${String(c.relayStallMs)} (entier de ${RELAIS_MS_MIN} à ${RELAIS_MS_MAX} ms, défaut ${RELAIS_STALL_MS_DEFAUT})`);
|
|
51
|
+
if (!total.valide) invalides.push(`relayMaxMs=${String(c.relayMaxMs)} (entier de ${RELAIS_MS_MIN} à ${RELAIS_MS_MAX} ms, défaut ${RELAIS_MAX_MS_DEFAUT})`);
|
|
52
|
+
return { plafond: plafond.valeur, stallMs: stall.valeur, maxMs: total.valeur, invalides };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
entierBorne, bornesRelais,
|
|
57
|
+
RELAIS_SIMULTANES_DEFAUT, RELAIS_SIMULTANES_MIN, RELAIS_SIMULTANES_MAX,
|
|
58
|
+
RELAIS_STALL_MS_DEFAUT, RELAIS_MAX_MS_DEFAUT, RELAIS_MS_MIN, RELAIS_MS_MAX,
|
|
59
|
+
};
|
package/server/handler.js
CHANGED
|
@@ -40,10 +40,22 @@ function init(ctx) {
|
|
|
40
40
|
// `init`, et un hôte a le même droit. On n'ajoute qu'une chose, on n'en fige aucune.
|
|
41
41
|
if (ctx && ctx.db) { const vu = Object.create(ctx); vu.db = mesures.observerBase(ctx.db); ctx = vu; }
|
|
42
42
|
PLAYER = ctx;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
// ⚠️ LA CONFIGURATION SE RELIT, L'ÉTAT VIVANT NE SE REMET PAS À ZÉRO. Cette ligne posait
|
|
44
|
+
// `relaisEnCours = 0` : un hôte qui rappelle `init` pendant qu'un relais est ouvert désarmait le
|
|
45
|
+
// plafond — la demande suivante partait vers l'amont avec l'unique place encore prise, et le
|
|
46
|
+
// `finally` de l'ancien relais rendait ensuite le compteur négatif (reproduit par un audit externe,
|
|
47
|
+
// cinquième passe, 13/09). Le compteur appartient au processus, pas au contexte : il ne se relit pas.
|
|
48
|
+
// Les bornes de temps, elles, sont capturées à l'ADMISSION de chaque relais (`relayerSousAdmission`) :
|
|
49
|
+
// un relais admis sous 30 s reste sous 30 s, quoi qu'un `init` ultérieur décide.
|
|
50
|
+
const bornes = bornesRelais(ctx && ctx.config);
|
|
51
|
+
plafondRelais = bornes.plafond;
|
|
52
|
+
relaisStallMs = bornes.stallMs;
|
|
53
|
+
relaisMaxMs = bornes.maxMs;
|
|
54
|
+
if (bornes.invalides.length) {
|
|
55
|
+
// Une fois, ici — pas à chaque relais : un réglage hors plage est une erreur de déploiement, dite
|
|
56
|
+
// à l'exploitant avec la plage, plutôt qu'un défaut appliqué en silence.
|
|
57
|
+
try { PLAYER.errors.capture(new Error(`réglages de relais hors plage, défauts appliqués : ${bornes.invalides.join(" ; ")}`), { route: "relais", benin: true }); } catch { /* jamais bloquant */ }
|
|
58
|
+
}
|
|
47
59
|
// Le domaine reçoit le même contexte : une seule construction pour tout le player.
|
|
48
60
|
require("./shares").init(ctx);
|
|
49
61
|
require("./retention").init(ctx);
|
|
@@ -69,8 +81,15 @@ const isAllowedStorageUrl = (url) => PLAYER.storage.isAllowedUrl(url);
|
|
|
69
81
|
// contexte (`config.maxConcurrentRelays`, défaut 64) : assez pour les requêtes Range parallèles de
|
|
70
82
|
// pdf.js, borné pour un processus. Sur serverless la plate-forme borne déjà la concurrence globale ;
|
|
71
83
|
// ceci protège le mode autonome et chaque instance chaude.
|
|
72
|
-
const RELAIS_SIMULTANES_DEFAUT =
|
|
84
|
+
const { bornesRelais, RELAIS_SIMULTANES_DEFAUT, RELAIS_STALL_MS_DEFAUT, RELAIS_MAX_MS_DEFAUT } = require("./bornes");
|
|
73
85
|
let plafondRelais = RELAIS_SIMULTANES_DEFAUT, relaisEnCours = 0;
|
|
86
|
+
// ⚠️ COMPTÉS, PARCE QU'UN HÔTE A CRU LES LIRE AILLEURS. On demandait aux hôtes de chercher « relais
|
|
87
|
+
// refusés » dans leurs journaux ; l'un d'eux a répondu par `lectureSaturee.total = 0` de la carte —
|
|
88
|
+
// qui ne compte que le cache de lecture, pas les relais (13/09). Une question qu'un hôte peut
|
|
89
|
+
// trancher par la carte ne doit pas être posée comme une fouille de journaux : la carte est
|
|
90
|
+
// structurée, datée, et répond même pour l'hôte qui ne lit jamais ses journaux. État du processus,
|
|
91
|
+
// comme `relaisEnCours` : jamais remis à zéro par `init`.
|
|
92
|
+
let relaisRefusesTotal = 0, dernierRefusRelais = null;
|
|
74
93
|
// ⚠️ UNE PLACE N'EST BORNÉE QUE SI LE RELAIS QUI L'OCCUPE FINIT. Un client qui cesse de lire — ou un
|
|
75
94
|
// amont qui cesse d'envoyer — laissait le pipeline en attente pour toujours : `finally` jamais atteint,
|
|
76
95
|
// place jamais rendue, et avec un plafond de 1, plus aucun fichier ne partait (reproduit par un audit
|
|
@@ -78,12 +97,12 @@ let plafondRelais = RELAIS_SIMULTANES_DEFAUT, relaisEnCours = 0;
|
|
|
78
97
|
// réponse — le commentaire de `bin/serve.js` affirmait le contraire. Deux bornes, configurables par
|
|
79
98
|
// le contexte : sans progression pendant `relayStallMs` (30 s), ou au-delà de `relayMaxMs` (15 min),
|
|
80
99
|
// le pipeline est ABANDONNÉ par signal — source amont détruite, réponse détruite, rejet, `finally`.
|
|
81
|
-
|
|
100
|
+
// ⚠️ Les valeurs viennent de `server/bornes.js` : entiers dans une plage écrite, jamais « tout nombre
|
|
101
|
+
// fini » — `setTimeout` ramène à 1 ms tout délai au-delà de 2 147 483 647 ms (audit, cinquième passe).
|
|
82
102
|
let relaisStallMs = RELAIS_STALL_MS_DEFAUT, relaisMaxMs = RELAIS_MAX_MS_DEFAUT;
|
|
83
|
-
const borneMs = (v, defaut) => { const n = Number(v); return Number.isFinite(n) && n > 0 ? n : defaut; };
|
|
84
|
-
function borneRelais(v) { const n = Math.trunc(Number(v)); return Number.isFinite(n) && n >= 1 ? n : RELAIS_SIMULTANES_DEFAUT; }
|
|
85
103
|
async function relayerSousAdmission(res, travail) {
|
|
86
104
|
if (relaisEnCours >= plafondRelais) {
|
|
105
|
+
relaisRefusesTotal += 1; dernierRefusRelais = Date.now();
|
|
87
106
|
// Une fois par heure, l'exploitant l'apprend : un 503 muet ressemble à une panne d'amont.
|
|
88
107
|
try {
|
|
89
108
|
if (await PLAYER.limits.allow("relais:sature-avert", 1, 3600)) {
|
|
@@ -94,8 +113,11 @@ async function relayerSousAdmission(res, travail) {
|
|
|
94
113
|
refuserEnTexte(res, 503, "Trop de transferts en cours, réessayez dans un instant");
|
|
95
114
|
return;
|
|
96
115
|
}
|
|
116
|
+
// Les bornes de CE relais sont figées ici : un `init` pendant le transfert relit la configuration
|
|
117
|
+
// pour les suivants, jamais pour celui-ci.
|
|
118
|
+
const bornes = { stallMs: relaisStallMs, maxMs: relaisMaxMs };
|
|
97
119
|
relaisEnCours += 1;
|
|
98
|
-
try { await travail(); } finally { relaisEnCours -= 1; }
|
|
120
|
+
try { await travail(bornes); } finally { relaisEnCours -= 1; }
|
|
99
121
|
}
|
|
100
122
|
|
|
101
123
|
// GREFFONS de ce studio — jamais du player. `null` quand le module est absent ou coupé
|
|
@@ -262,7 +284,7 @@ const { refuserEnTexte, repondreJson, repondreJsonTexte } = require("./reponses.
|
|
|
262
284
|
// exemplaires d'un fait divergent, c'est la formule du dépôt.
|
|
263
285
|
const POLITIQUE_PERMISSIONS = "camera=(), microphone=(), geolocation=(), payment=()";
|
|
264
286
|
|
|
265
|
-
async function relayerFichier(res, r, disposition) {
|
|
287
|
+
async function relayerFichier(res, r, disposition, bornes = { stallMs: relaisStallMs, maxMs: relaisMaxMs }) {
|
|
266
288
|
if (!r) { refuserEnTexte(res, 404, "Fichier indisponible"); return; }
|
|
267
289
|
// 413 et 416 sont des REFUS ARGUMENTÉS de l'amont local (plafond, borne absurde) : les fondre
|
|
268
290
|
// dans un 502 dirait « panne » là où l'amont a dit « demande irrecevable ».
|
|
@@ -347,8 +369,8 @@ async function relayerFichier(res, r, disposition) {
|
|
|
347
369
|
// ne se réarme jamais. L'abandon passe par le signal du pipeline, qui détruit la source ET la réponse.
|
|
348
370
|
const abandon = new globalThis.AbortController();
|
|
349
371
|
let stall = null;
|
|
350
|
-
const rearmer = () => { clearTimeout(stall); stall = setTimeout(() => abandon.abort(new Error(`relais abandonné : aucune progression depuis ${
|
|
351
|
-
const budget = setTimeout(() => abandon.abort(new Error(`relais abandonné : plus de ${
|
|
372
|
+
const rearmer = () => { clearTimeout(stall); stall = setTimeout(() => abandon.abort(new Error(`relais abandonné : aucune progression depuis ${bornes.stallMs} ms`)), bornes.stallMs); };
|
|
373
|
+
const budget = setTimeout(() => abandon.abort(new Error(`relais abandonné : plus de ${bornes.maxMs} ms au total`)), bornes.maxMs);
|
|
352
374
|
rearmer();
|
|
353
375
|
try {
|
|
354
376
|
await pipeline(Readable.fromWeb(r.body), async function* (source) {
|
|
@@ -817,6 +839,15 @@ async function handlerMesure(req, res) {
|
|
|
817
839
|
derniereIlYaS: dernier == null ? null : Math.max(0, Math.round((Date.now() - dernier) / 1000)),
|
|
818
840
|
};
|
|
819
841
|
})(),
|
|
842
|
+
// ⚠️ MÊME FORME, AUTRE PLAFOND. `lectureSaturee` est le cache de lecture ; ceci est l'admission
|
|
843
|
+
// des relais de fichiers (`config.maxConcurrentRelays`). Un hôte a lu le premier pour le second
|
|
844
|
+
// (13/09) parce que le second n'existait pas sur la carte — et `mesures.statuts.occupe503`
|
|
845
|
+
// les confond. Trois clés, jamais séparées : un total sans sa fenêtre ment par omission.
|
|
846
|
+
relaisRefuses: {
|
|
847
|
+
total: relaisRefusesTotal,
|
|
848
|
+
fenetreS: Math.round(process.uptime()),
|
|
849
|
+
derniereIlYaS: dernierRefusRelais == null ? null : Math.max(0, Math.round((Date.now() - dernierRefusRelais) / 1000)),
|
|
850
|
+
},
|
|
820
851
|
// ⚠️ CE QUE CETTE INSTANCE A VÉCU — parce que `lectureSaturee` ne répondait qu'à UNE
|
|
821
852
|
// question. « La route est-elle lente ? », « lesquelles ? », « la base ou nous ? »,
|
|
822
853
|
// « combien de 5xx ? », « la boucle décroche-t-elle ? » n'avaient aucune réponse
|
|
@@ -1037,9 +1068,9 @@ async function handlerMesure(req, res) {
|
|
|
1037
1068
|
if (String(q.file || "") === "1") {
|
|
1038
1069
|
if (!isAllowedStorageUrl(pres.file_url)) { refuserEnTexte(res, 404, "Fichier indisponible"); return; }
|
|
1039
1070
|
const range = req.headers["range"];
|
|
1040
|
-
await relayerSousAdmission(res, async () => {
|
|
1071
|
+
await relayerSousAdmission(res, async (bornes) => {
|
|
1041
1072
|
const r = await PLAYER.storage.fetchFile(pres.file_url, { range });
|
|
1042
|
-
await relayerFichier(res, r, null);
|
|
1073
|
+
await relayerFichier(res, r, null, bornes);
|
|
1043
1074
|
});
|
|
1044
1075
|
return;
|
|
1045
1076
|
}
|
|
@@ -1062,9 +1093,9 @@ async function handlerMesure(req, res) {
|
|
|
1062
1093
|
if (!isAllowedStorageUrl(url)) return sendRefusal(res, "url-not-allowed", embed);
|
|
1063
1094
|
if (String(q.stream || "") === "1") {
|
|
1064
1095
|
const range = req.headers["range"];
|
|
1065
|
-
await relayerSousAdmission(res, async () => {
|
|
1096
|
+
await relayerSousAdmission(res, async (bornes) => {
|
|
1066
1097
|
const r = await PLAYER.storage.fetchFile(url, { range });
|
|
1067
|
-
await relayerFichier(res, r, dispositionInline(q.name));
|
|
1098
|
+
await relayerFichier(res, r, dispositionInline(q.name), bornes);
|
|
1068
1099
|
});
|
|
1069
1100
|
return;
|
|
1070
1101
|
}
|
|
@@ -1116,9 +1147,9 @@ async function handlerMesure(req, res) {
|
|
|
1116
1147
|
// Stream depuis le Storage en RELAYANT les requêtes Range → pdf.js charge progressivement (les 1res
|
|
1117
1148
|
// pages s'affichent sans télécharger tout le PDF) → affichage bien plus rapide.
|
|
1118
1149
|
const range = req.headers["range"];
|
|
1119
|
-
await relayerSousAdmission(res, async () => {
|
|
1150
|
+
await relayerSousAdmission(res, async (bornes) => {
|
|
1120
1151
|
const r = await PLAYER.storage.fetchFile(share.file_url, { range });
|
|
1121
|
-
await relayerFichier(res, r, dispositionInline(share.file_name));
|
|
1152
|
+
await relayerFichier(res, r, dispositionInline(share.file_name), bornes);
|
|
1122
1153
|
});
|
|
1123
1154
|
return;
|
|
1124
1155
|
}
|
|
@@ -1211,6 +1242,9 @@ async function handlerMesure(req, res) {
|
|
|
1211
1242
|
// regardant si le corps a été lu, ce qu aucune route ne peut montrer de l extérieur.
|
|
1212
1243
|
// ⚠️ Exporté pour être ÉPROUVÉ : « le contexte reste vivant » ne se vérifie pas de l'extérieur.
|
|
1213
1244
|
module.exports = { __contexte: () => PLAYER, handler, init, TIERS, POLITIQUE_PERMISSIONS, refuserEnTexte, repondreJson, __relayerFichier: relayerFichier, __jsonPourScript: jsonPourScript,
|
|
1245
|
+
// ⚠️ COUTURE DE BANC : le compteur de relais en vol est un état du PROCESSUS, jamais remis à zéro par
|
|
1246
|
+
// `init`. Un banc doit pouvoir vérifier qu'il revient à zéro et ne passe jamais sous zéro.
|
|
1247
|
+
__relaisEnCours: () => relaisEnCours,
|
|
1214
1248
|
// ⚠️ COUTURE DE BANC, PAS D'API : le cache de lecture est global au module, et un banc qui laisse des
|
|
1215
1249
|
// lectures en vol contamine le suivant (128 promesses éternelles, 503 partout — trouvé par un audit
|
|
1216
1250
|
// externe sous mélange, graine 20260913). Un banc doit pouvoir VÉRIFIER qu'il rend le cache vide.
|
|
@@ -20,9 +20,11 @@ let PLAYER = null;
|
|
|
20
20
|
// Deux dimensions pour le code, parce qu'une seule se contourne : par ADRESSE (une adresse ne
|
|
21
21
|
// recommence pas à zéro en changeant d'email) et par IDENTITÉ (plusieurs adresses ne forcent pas un
|
|
22
22
|
// même email). Les compteurs sont pris À L'ADMISSION, donc réussite, échec et exception les
|
|
23
|
-
// consomment pareil. L'identité est
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// consomment pareil. L'identité n'est jamais l'email : la table des compteurs n'a pas à porter
|
|
24
|
+
// d'adresses en clair. La clé vient du GREFFON (un HMAC avec un secret chez l'hôte, ci-dessous) ;
|
|
25
|
+
// l'empreinte SHA-256 n'est que le repli, et il est dit. ⚠️ Ce paragraphe affirmait « le cœur n'a
|
|
26
|
+
// pas de secret de serveur, donc une empreinte, pas un HMAC » — retiré (cinquième passe de l'audit,
|
|
27
|
+
// 13/09) : trop absolu, et surtout la clé n'a pas besoin d'un secret du cœur, elle vient de l'hôte.
|
|
26
28
|
const VERIF_PAR_ADRESSE = 100, VERIF_PAR_IDENTITE = 10, VERIF_FENETRE_IDENTITE_S = 900;
|
|
27
29
|
const GOOGLE_PAR_ADRESSE = 100, DEMANDE_PAR_IDENTITE = 5;
|
|
28
30
|
// ⚠️ UN SHA-256 D'EMAIL N'EST PAS UNE ANONYMISATION : il se renverse par dictionnaire — qui lit la
|
package/types/context.d.ts
CHANGED
|
@@ -128,13 +128,17 @@ export interface Reglages {
|
|
|
128
128
|
supabasePublishableKey: string;
|
|
129
129
|
mapsKey: string;
|
|
130
130
|
extraFrameAncestors: string[];
|
|
131
|
-
/** Transferts de fichiers relayés simultanément par processus (défaut 64
|
|
132
|
-
*
|
|
131
|
+
/** Transferts de fichiers relayés simultanément par processus (défaut 64 ; entier de 1 à 1024,
|
|
132
|
+
* sinon le défaut, dit une fois à `init`). Au-delà, le relais répond 503 + Retry-After AVANT
|
|
133
|
+
* l'appel amont ; la place est rendue en `finally`. 64 n'est pas sûr partout : 64 × 8 Mio avec
|
|
134
|
+
* des clients lents font monter la RSS de 130 à 194 Mio — 16 à 32 sur un processus à 256 Mio. */
|
|
133
135
|
maxConcurrentRelays?: number;
|
|
134
136
|
/** Un relais sans progression pendant ce délai est abandonné — source et réponse détruites, place
|
|
135
|
-
* rendue (défaut 30 000 ms
|
|
137
|
+
* rendue (défaut 30 000 ms ; entier de 1 à 86 400 000 ms, sinon le défaut, dit une fois à `init` :
|
|
138
|
+
* `setTimeout` ramène à 1 ms tout délai au-delà de 2 147 483 647 ms). `requestTimeout` ne borne
|
|
139
|
+
* pas l'émission d'une réponse. */
|
|
136
140
|
relayStallMs?: number;
|
|
137
|
-
/** Durée totale maximale d'un relais (défaut 900 000 ms). */
|
|
141
|
+
/** Durée totale maximale d'un relais (défaut 900 000 ms ; même plage que `relayStallMs`). */
|
|
138
142
|
relayMaxMs?: number;
|
|
139
143
|
/** Ce qui est POSÉ, à côté de ce que le code SAIT faire : la carte d'identité publie les deux,
|
|
140
144
|
* parce qu'une capacité disponible mais non configurée se comporte comme une absence. */
|