discovery-media-player 0.1.164 → 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/bin/serve.js +42 -7
- package/context/standalone.js +12 -0
- package/docs/HOST-CONTRACT.md +118 -1
- package/docs/RETENTION.md +6 -0
- package/package.json +1 -1
- package/server/bornes.js +59 -0
- package/server/browser.generated.js +1 -1
- package/server/gabarit-agent.js +1 -0
- package/server/gabarit-live.js +23 -14
- package/server/handler.js +119 -14
- package/server/page-visionneuse.js +54 -3
- package/server/retention.js +28 -4
- package/server/routes-liens.js +23 -3
- package/server/routes-visiteur.js +55 -1
- package/types/context.d.ts +12 -0
package/bin/serve.js
CHANGED
|
@@ -179,7 +179,21 @@ async function servir(req, res) {
|
|
|
179
179
|
// Le player lit `req.query` (convention des plateformes serverless) et `req.body` déjà analysé.
|
|
180
180
|
req.query = q;
|
|
181
181
|
if (req.method === "POST") {
|
|
182
|
-
|
|
182
|
+
// ⚠️ TROIS ISSUES DISTINCTES, TROIS RÉPONSES — un corps invalide, trop gros ou coupé rendait
|
|
183
|
+
// `{}` dans les trois cas, et le gestionnaire répondait « bad-event » à un client qui n'avait
|
|
184
|
+
// qu'envoyé un fichier trop lourd. 413 pour le dépassement (avec `Connection: close` : on ne
|
|
185
|
+
// draine pas un corps sans fin), 400 pour un JSON illisible, rien pour une connexion partie.
|
|
186
|
+
// Relevé par un audit externe le 13/09.
|
|
187
|
+
const lu = await lireCorpsJson(req);
|
|
188
|
+
if (lu.etat === "too-large") {
|
|
189
|
+
res.setHeader("Connection", "close");
|
|
190
|
+
res.once("finish", () => { try { req.destroy(); } catch { /* déjà parti */ } });
|
|
191
|
+
player.refuserEnTexte(res, 413, "Corps trop volumineux (1 Mo au plus)");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (lu.etat === "invalid-json") { player.refuserEnTexte(res, 400, "Corps JSON illisible"); return; }
|
|
195
|
+
if (lu.etat === "aborted") { try { res.end(); } catch { /* flux clos */ } return; }
|
|
196
|
+
req.body = lu.corps;
|
|
183
197
|
}
|
|
184
198
|
|
|
185
199
|
try {
|
|
@@ -202,22 +216,43 @@ const serveur = http.createServer((req, res) => {
|
|
|
202
216
|
player.refuserEnTexte(res, 500, "Erreur");
|
|
203
217
|
});
|
|
204
218
|
});
|
|
219
|
+
// ⚠️ LES DÉLAIS DE NODE (300 s par requête, 60 s pour les en-têtes) SONT CEUX D'UN SERVEUR DERRIÈRE UN
|
|
220
|
+
// PROXY, PAS D'UN SERVEUR EXPOSÉ. Mesurés par un audit externe le 13/09 : `requestTimeout` 300 000,
|
|
221
|
+
// `headersTimeout` 60 000. Une connexion qui envoie ses en-têtes au goutte-à-goutte tenait donc une
|
|
222
|
+
// minute, un corps lent cinq — par socket. ⚠️ Ces bornes portent sur la REQUÊTE : `requestTimeout`
|
|
223
|
+
// ne couvre PAS l'émission d'une réponse (cette phrase affirmait que « trente secondes couvrent un
|
|
224
|
+
// relais de 60 Mo » — faux, relevé par le même audit) ; un relais lent est borné par ses propres
|
|
225
|
+
// délais de progression et de budget (`relayStallMs`, `relayMaxMs` dans le gestionnaire). Les
|
|
226
|
+
// en-têtes n'ont aucune raison de prendre plus de quinze secondes ; un keep-alive court rend les
|
|
227
|
+
// sockets. Un proxy amont peut serrer davantage, jamais l'inverse.
|
|
228
|
+
serveur.requestTimeout = 30_000;
|
|
229
|
+
serveur.headersTimeout = 15_000;
|
|
230
|
+
serveur.keepAliveTimeout = 5_000;
|
|
205
231
|
|
|
206
232
|
/** Corps JSON, borné. Un corps sans fin est une façon peu coûteuse de faire tomber un serveur. */
|
|
233
|
+
/**
|
|
234
|
+
* Rend `{ etat, corps }` : `ok` (corps = l'objet lu, `{}` pour un corps vide), `too-large` (la lecture
|
|
235
|
+
* s'arrête au premier octet au-delà de `maxOctets`, sans drainer la suite), `invalid-json`, ou
|
|
236
|
+
* `aborted` (la connexion est partie avant la fin). L'appelant choisit le code HTTP : ici, on ne sait
|
|
237
|
+
* que lire.
|
|
238
|
+
*/
|
|
207
239
|
function lireCorpsJson(req, maxOctets = 1_000_000) {
|
|
208
240
|
return new Promise((resolve) => {
|
|
209
|
-
let taille = 0;
|
|
241
|
+
let taille = 0, regle = false;
|
|
210
242
|
const morceaux = [];
|
|
243
|
+
const rendre = (v) => { if (!regle) { regle = true; resolve(v); } };
|
|
211
244
|
req.on("data", (c) => {
|
|
245
|
+
if (regle) return;
|
|
212
246
|
taille += c.length;
|
|
213
|
-
if (taille > maxOctets) { req.
|
|
247
|
+
if (taille > maxOctets) { try { req.pause(); } catch { /* déjà clos */ } rendre({ etat: "too-large", corps: null }); return; }
|
|
214
248
|
morceaux.push(c);
|
|
215
249
|
});
|
|
216
250
|
req.on("end", () => {
|
|
217
|
-
try {
|
|
218
|
-
catch {
|
|
251
|
+
try { rendre({ etat: "ok", corps: JSON.parse(Buffer.concat(morceaux).toString("utf8") || "{}") }); }
|
|
252
|
+
catch { rendre({ etat: "invalid-json", corps: null }); }
|
|
219
253
|
});
|
|
220
|
-
req.on("error", () =>
|
|
254
|
+
req.on("error", () => rendre({ etat: "aborted", corps: null }));
|
|
255
|
+
req.on("aborted", () => rendre({ etat: "aborted", corps: null }));
|
|
221
256
|
});
|
|
222
257
|
}
|
|
223
258
|
|
|
@@ -286,4 +321,4 @@ if (require.main === module) {
|
|
|
286
321
|
});
|
|
287
322
|
}
|
|
288
323
|
|
|
289
|
-
module.exports = { serveur, servir, versParametres, pageAccueil, __arreterProprement: arreterProprement, DELAI_ARRET_MS };
|
|
324
|
+
module.exports = { serveur, servir, versParametres, pageAccueil, lireCorpsJson, __arreterProprement: arreterProprement, DELAI_ARRET_MS };
|
package/context/standalone.js
CHANGED
|
@@ -840,6 +840,18 @@ function createStandaloneContext(env = process.env) {
|
|
|
840
840
|
supabasePublishableKey: env.SUPABASE_PUBLISHABLE_KEY || "",
|
|
841
841
|
mapsKey: env.GOOGLE_MAPS_API_KEY || "",
|
|
842
842
|
extraFrameAncestors: String(env.DOC_FRAME_ANCESTORS || "").split(/\s+/).filter(Boolean),
|
|
843
|
+
// Transferts de fichiers simultanés par processus (défaut 64) : le relais refuse en 503 au-delà,
|
|
844
|
+
// avant tout appel amont. Lu ICI, pas dans le cœur — la configuration entre par le contexte.
|
|
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,
|
|
851
|
+
// Un relais sans progression pendant relayStallMs, ou plus long que relayMaxMs, est abandonné
|
|
852
|
+
// (source et réponse détruites) : sans ça, un client qui cesse de lire garde sa place pour toujours.
|
|
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,
|
|
843
855
|
|
|
844
856
|
/**
|
|
845
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
|
|
@@ -410,6 +423,56 @@ inheritance both refuse — without either of them knowing why.
|
|
|
410
423
|
Requires `supabase/migrations/0001-destinataire-atteste.sql`. Until it is applied the player refuses
|
|
411
424
|
the attested creation and names the file; it never falls back to the other column.
|
|
412
425
|
|
|
426
|
+
## The visitor wall (`plugins.visitors`): what the player counts, and what your plugin must do
|
|
427
|
+
|
|
428
|
+
A host can gate documents behind a soft wall — an e-mail code, or a Google credential — by providing
|
|
429
|
+
`plugins.visitors` with `requestCode(email, { title })`, `verifyCode(email, code, name)` and
|
|
430
|
+
`verifyGoogle(credential)`. The player exposes them as `visitor-request`, `visitor-verify` and
|
|
431
|
+
`visitor-google`.
|
|
432
|
+
|
|
433
|
+
⚠️ **Until this train, only the request was rate-limited; verification called your plugin directly.**
|
|
434
|
+
An external audit reproduced 1 000 code attempts and 1 000 Google verifications from one address with
|
|
435
|
+
zero limiter calls (13/09). A short code with no counter in the plugin was brute-forceable, and a
|
|
436
|
+
Google verification per anonymous request was a network amplifier. The player no longer assumes your
|
|
437
|
+
plugin counts — the same rule as the assistant's session↔document binding: a security property must
|
|
438
|
+
not depend on code the player does not contain.
|
|
439
|
+
|
|
440
|
+
| action | per address | per identity (fingerprint of the normalised e-mail, never the address) |
|
|
441
|
+
|---|---|---|
|
|
442
|
+
| `visitor-request` | 20 / hour | 5 / hour |
|
|
443
|
+
| `visitor-verify` | 100 / hour | 10 / 15 minutes |
|
|
444
|
+
| `visitor-google` | 100 / hour | — |
|
|
445
|
+
|
|
446
|
+
Counters are taken **at admission**: success, failure and an exception in your plugin consume them
|
|
447
|
+
alike. Beyond a limit the answer is `429 { error: "rate" }` and **your plugin is not called**.
|
|
448
|
+
|
|
449
|
+
⚠️ **Provide `rateLimitKey(email): Promise<string>` on the plugin — the identity key should be
|
|
450
|
+
yours.** Without it the player keys the per-identity counters on a truncated SHA-256 of the
|
|
451
|
+
normalised e-mail: `player_rate_limits` never carries an address in clear, but a fingerprint is a
|
|
452
|
+
**pseudonym, not a secret** — anyone reading that table, a backup or an admin tool can precompute the
|
|
453
|
+
fingerprints of likely addresses (an audit showed it on 13/09). Your implementation should be a
|
|
454
|
+
stable, opaque HMAC with a host-side secret and **domain separation**:
|
|
455
|
+
`HMAC(secret, "visitor-email\0" + emailNormalised)`. The player calls it with the e-mail already
|
|
456
|
+
trimmed and lower-cased, prefixes your key with `h:` (a fallback fingerprint gets `e:`, so the two
|
|
457
|
+
never collide), and truncates it to 64 characters. If the capability is absent, throws, or returns
|
|
458
|
+
anything but a non-empty string, the player **falls back to the fingerprint and reports it once per
|
|
459
|
+
process** through `errors.capture` (`benin: true`): refusing to limit would be worse than limiting
|
|
460
|
+
under a weak pseudonym, and silence would be worse than both. (An earlier version of this paragraph
|
|
461
|
+
said the player holds no server secret at all — too absolute: the standalone context already carries
|
|
462
|
+
`ipHashSecret` for another purpose. The key still belongs with you, not with that secret.)
|
|
463
|
+
|
|
464
|
+
⚠️ **What your plugin must still guarantee — the player cannot do it for you:**
|
|
465
|
+
|
|
466
|
+
- the code is **short-lived** (minutes, not hours) and **single-use**: a code that stays valid after a
|
|
467
|
+
successful verification can be replayed from a shoulder-surfed screen;
|
|
468
|
+
- the code has enough entropy for 10 attempts per quarter-hour not to be a lottery — six digits give
|
|
469
|
+
one chance in 100 000 per attempt at that pace, which is acceptable; four digits are not;
|
|
470
|
+
- `verifyGoogle` validates the credential's audience and issuer server-side, and does not accept an
|
|
471
|
+
expired token.
|
|
472
|
+
|
|
473
|
+
The player's counters bound the *rate*; your plugin bounds the *code*. Both are needed, and neither
|
|
474
|
+
replaces the other.
|
|
475
|
+
|
|
413
476
|
## ⚠️ What `limits.allow` promises changed
|
|
414
477
|
|
|
415
478
|
It used to promise *best effort, per process*. The standalone context now counts in a **shared
|
|
@@ -712,6 +775,24 @@ Four requirements, in order of what they cost when missed:
|
|
|
712
775
|
anywhere. Announce the length of what you send, request `Accept-Encoding: identity`, and refuse
|
|
713
776
|
a compressed `206` — range bounds refer to compressed bytes.
|
|
714
777
|
2. **Relay `Range`** (`206` + `Accept-Ranges: bytes`). Progressive loading depends on it.
|
|
778
|
+
⚠️ And expect the player to hold **at most `config.maxConcurrentRelays` relays open per process**
|
|
779
|
+
(default 64; `PLAYER_MAX_RELAYS` in the standalone context): above it the player answers **503
|
|
780
|
+
with `Retry-After: 2` before calling you**, with no queue. The stream bounded bytes; nothing
|
|
781
|
+
bounded how many streams were open — an audit opened 200 slow transfers and got 200 upstream
|
|
782
|
+
connections (13/09). The slot is released in a `finally`, so your errors and a client leaving
|
|
783
|
+
mid-stream give it back — and so does a relay that **stops progressing**: no chunk for
|
|
784
|
+
`config.relayStallMs` (30 s) or a total beyond `config.relayMaxMs` (15 min) aborts the pipeline,
|
|
785
|
+
destroying your response and the client's. A client that stops reading no longer keeps a slot
|
|
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.
|
|
715
796
|
3. **Accept a server-to-server call.** A tracked link is opened by someone with no session on your
|
|
716
797
|
side. Authenticate the player with the shared secret in the `x-player-fetch-secret` **header** —
|
|
717
798
|
header only, never a query string: logs keep URLs.
|
|
@@ -762,6 +843,23 @@ back on an inability to *reach*.** And "do not fall back" applies to what you **
|
|
|
762
843
|
|
|
763
844
|
## What will bite
|
|
764
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
|
+
|
|
854
|
+
⚠️ **Every capability you provide must settle in bounded time — `db.request` first of all.** The
|
|
855
|
+
player awaits your `db.request`, `storage.fetchFile`, `mail.send` and the visitor plugin; a promise
|
|
856
|
+
that never settles keeps a request in flight, and the read cache admits at most 128 in-flight reads
|
|
857
|
+
per process before answering **503 busy** to everyone. A database call that hangs is therefore not
|
|
858
|
+
"slow", it is an availability incident for the whole instance — the exact mechanism an audit
|
|
859
|
+
reproduced inside the test suite with a never-settling promise (13/09). Time out your own calls
|
|
860
|
+
(the standalone context bounds its own with `AbortSignal`), and never return a promise you cannot
|
|
861
|
+
guarantee will settle.
|
|
862
|
+
|
|
765
863
|
**Your document-opening doors reappear.** A host has more than one place that opens a file, and new
|
|
766
864
|
ones get written. Keep the list and hunt it periodically — and note that **your search criteria
|
|
767
865
|
decide what you find**: search by what the user *obtains* (a document opens), not by the technique
|
|
@@ -834,12 +932,18 @@ late, a caller reading `sent: false` retries, creating a **second child link and
|
|
|
834
932
|
| `delivery` | what happened | what to do |
|
|
835
933
|
|---|---|---|
|
|
836
934
|
| `"sent"` | your mail path reported success | nothing |
|
|
837
|
-
| `"refused"` | a decision was made — yours or ours; `sendRefused` names it | surface the reason; retrying will refuse again |
|
|
935
|
+
| `"refused"` | a decision was made — yours or ours; `sendRefused` names it, and when your mail hook answered `{ sent: false, reason }` (or `motif`) that word comes back as `hostReason`, trimmed to 80 characters | surface the reason; retrying will refuse again |
|
|
838
936
|
| `"unknown"` | the call failed (timeout, network). **We do not know whether the mail went out** | surface it to a human. **Do not retry automatically** — a retry may duplicate the email |
|
|
839
937
|
| `"not-requested"` | `send` was falsy | nothing |
|
|
840
938
|
|
|
841
939
|
`sent` is unchanged for integrations already reading it.
|
|
842
940
|
|
|
941
|
+
⚠️ **Your refusal reason was being thrown away.** One host answers every refusal with
|
|
942
|
+
`{ sent: false, motif }` — eight distinct reasons — precisely so that *refused* never reads as *down*;
|
|
943
|
+
the route read only `sent`. From this train on, a string `reason` or `motif` on your hook's answer travels
|
|
944
|
+
back to the caller as `hostReason` (a string, at most 80 characters; an object is ignored). It is
|
|
945
|
+
your word to your own caller, not a channel: keep it short and non-sensitive.
|
|
946
|
+
|
|
843
947
|
⚠️ **And you can now make the retry safe: pass a `clientKey`.** Two `reshare` calls with the same
|
|
844
948
|
parent, the same recipient and the same `clientKey` return the **same child link** and send **one**
|
|
845
949
|
email — the second answers `delivery: "idempotent"`, which means *"this was already done"*, not
|
|
@@ -875,6 +979,11 @@ Three things changed, and the second one is the one you may notice:
|
|
|
875
979
|
- **The renderer refuses the same URLs again**, because one path never reaches this server: a
|
|
876
980
|
participant can broadcast presence over Realtime straight to the other viewers. No server-side
|
|
877
981
|
barrier can see that, so the check also lives where every path converges — at render time.
|
|
982
|
+
- ⚠️ **`data:` and `blob:` URLs are refused too, deliberately.** They reach no one, but a data URL
|
|
983
|
+
travels inside every message row and every presence broadcast, and one more "harmless" form is
|
|
984
|
+
one more form to reason about at the next audit. A host that stores avatars as data URLs — as an
|
|
985
|
+
offline fallback, say — will see initials, and nothing will say why except this line. (Asked for by
|
|
986
|
+
a host, 13/09: three of its members carry one.)
|
|
878
987
|
|
|
879
988
|
⚠️ **What your `storage.remove` returns now decides whether a row survives.** It returns a boolean:
|
|
880
989
|
`true` means the object is gone, `false` means it is still there. Until 0.1.163 the retention sweep
|
|
@@ -893,6 +1002,14 @@ permanently. The sweep now **keeps the row** when `remove` returns `false`, and
|
|
|
893
1002
|
- **`retenues > 0` in a retention report means your provider refused a removal**, not that the purge
|
|
894
1003
|
is broken. The next pass retries. A row that lingers is recoverable; a file whose only pointer was
|
|
895
1004
|
erased is not.
|
|
1005
|
+
- ⚠️ **If you provide no `storage.remove` at all, file-bearing rows are retained too — and the report
|
|
1006
|
+
says so.** There were three states, not two: `true`, `false`, and *not attempted*. Until 0.1.164 the
|
|
1007
|
+
third one let the row go "as before" — so a host providing `put` without `remove` manufactured
|
|
1008
|
+
permanently unreachable objects at every sweep, with no counter moving. A host found it by reading
|
|
1009
|
+
`retention.js`, not this paragraph, which assumed you provide one. Now: the row stays, `retenues`
|
|
1010
|
+
counts it, the report carries `sansRemove: true` (in `dryRun` too, so you can read it before arming
|
|
1011
|
+
the sweep), and the missing capability is reported once per process through `errors.capture` with
|
|
1012
|
+
`benin: true`. Provide `storage.remove` and the next pass lets them go.
|
|
896
1013
|
|
|
897
1014
|
**If you write to the `tts-cache` bucket yourself, write the trace too.** Retention removes an object
|
|
898
1015
|
only when its fingerprint has a row in `doc_tts_objects`, and only the player's own route writes that
|
package/docs/RETENTION.md
CHANGED
|
@@ -366,6 +366,12 @@ what keeps them out of this tool's candidate list.
|
|
|
366
366
|
by an external audit on 2026-09-12, reproduced before being fixed. A retained row is recoverable —
|
|
367
367
|
the next pass retries it; a lost file is not. Read `retenues > 0` as *"the storage provider refused
|
|
368
368
|
a removal; look at it"*, not as a purge failure.
|
|
369
|
+
- ⚠️ **And "not attempted" is the third state, found by a host on 0.1.164.** A host providing `put`
|
|
370
|
+
without `remove` had no capability to refuse with: `retirerFichier` answered `null` and the row
|
|
371
|
+
went "as before" — the irreversible loss above, through the other door. Now a missing
|
|
372
|
+
`storage.remove` retains every file-bearing row, counts it in `retenues`, sets `sansRemove: true` on
|
|
373
|
+
the result (also in `dryRun`), and is reported once per process (`errors.capture`, `benin: true`).
|
|
374
|
+
A row without a file still goes.
|
|
369
375
|
- ⚠️ **The alignment `.json` never retains anything — only the audio does.** A third of fingerprints
|
|
370
376
|
legitimately have no companion (see the 552/356 measurement above); gating the row on both objects
|
|
371
377
|
would hold a third of the cache forever to protect files that do not exist. So the `.mp3` alone
|
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
|
+
};
|
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
// Injecté par api/doc.js dans un <script nonce=…> de la visionneuse. Le code navigateur du
|
|
7
7
|
// player vit en modules TypeScript testés sous player/src/ — c'est ici qu'il atterrit.
|
|
8
8
|
"use strict";
|
|
9
|
-
module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var L=Object.defineProperty,Ke=Object.defineProperties,Ye=Object.getOwnPropertyDescriptor,Ze=Object.getOwnPropertyDescriptors,qe=Object.getOwnPropertyNames,Ee=Object.getOwnPropertySymbols;var Te=Object.prototype.hasOwnProperty,Qe=Object.prototype.propertyIsEnumerable;var _e=(e,n,t)=>n in e?L(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t,Q=(e,n)=>{for(var t in n||(n={}))Te.call(n,t)&&_e(e,t,n[t]);if(Ee)for(var t of Ee(n))Qe.call(n,t)&&_e(e,t,n[t]);return e},Se=(e,n)=>Ke(e,Ze(n));var w=(e,n)=>{for(var t in n)L(e,t,{get:n[t],enumerable:!0})},Xe=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of qe(n))!Te.call(e,o)&&o!==t&&L(e,o,{get:()=>n[o],enumerable:!(r=Ye(n,o))||r.enumerable});return e};var Je=e=>Xe(L({},\"__esModule\",{value:!0}),e);var at={};w(at,{bridge:()=>H,cadence:()=>$,chat:()=>F,live:()=>V,presentation:()=>W,presentationState:()=>j,tracking:()=>D,viewer:()=>B});var H={};w(H,{onHostMessage:()=>cn,onPlayerMessage:()=>ln,parseHostMessage:()=>Ae,parsePlayerMessage:()=>Re,sendToHost:()=>an,sendToPlayer:()=>sn,toWire:()=>J});var X=\"3dd-doc-\",en=/^[A-Za-z0-9_-]{1,64}$/,nn=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],tn=[\"embed-denied\"],rn=[\"present-invite\",\"present-handover\",\"present-switch\"],on=[\"handover-done\"];function J(e){let n={type:X+e.type};return\"slug\"in e&&e.slug&&(n.slug=e.slug),\"reason\"in e&&e.reason&&(n.reason=String(e.reason).slice(0,40)),n}function ke(e,n,t){if(!e||typeof e!=\"object\")return null;let r=e.type;if(typeof r!=\"string\"||!r.startsWith(X))return null;let o=r.slice(X.length);if(n.includes(o))return{type:o};if(tn.includes(o)){let s=e.reason;return{type:o,reason:/^[a-z-]{1,40}$/.test(String(s||\"\"))?String(s):\"unknown\"}}if(!t.includes(o))return null;let a=e.slug;return typeof a!=\"string\"||!en.test(a)?null:{type:o,slug:a}}function Re(e){return ke(e,nn,rn)}function Ae(e){return ke(e,on,[])}function an(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(J(e),\"*\")}catch(t){}}function sn(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(J(n),\"*\")}catch(r){}}function un(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function ln(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=a=>{if(!un(t,a.source))return;let s=Re(a.data);s&&e(s)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function cn(e,n){var a;let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=(a=t.parent)!=null?a:null,o=s=>{if(!r||r===t||s.source!==r)return;let u=Ae(s.data);u&&e(u)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var D={};w(D,{createTracker:()=>xn});var $={};w($,{PRESENTER_ACTIONS_PER_HOUR:()=>te,PRESENT_CACHE_MS:()=>fn,PRESENT_QUOTA_MARGIN:()=>Ue,PRESENT_QUOTA_PER_HOUR:()=>gn,PRESENT_READS_PER_HOUR:()=>Oe,PRESENT_READ_BURST:()=>Le,PRESENT_READ_COALESCE_MS:()=>He,PRESENT_RESYNC_MS:()=>Ce,PRESENT_SIGNAL_BUDGET_PER_HOUR:()=>pn,PRESENT_WRITE_TIMEOUT_MS:()=>re,READERS_PER_EGRESS:()=>U,RESYNC_READS_PER_HOUR:()=>Ne,SESSION_IDLE_MS:()=>ne,SESSION_INTERVAL_MS:()=>ee,SESSION_QUOTA_PER_HOUR:()=>dn,SESSION_WRITES_PER_HOUR:()=>Ie,VIEW_EVENTS_PER_READER_HOUR:()=>Pe,VIEW_QUOTA_PER_HOUR:()=>mn});var ee=3e4,Ie=Math.ceil(120),U=25,dn=Ie*U,Pe=720,mn=Pe*U,ne=18e4,Ce=25e3,Ne=Math.ceil(36e5/Ce),te=720,Oe=Ne+te,pn=te,Le=20,re=1e4,He=400,fn=He,Ue=4,gn=(Oe+Le)*U*Ue;var bn=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function hn(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(16));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}return Math.random().toString(36).slice(2)+Date.now().toString(36)}function yn(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;return o&&typeof o.sendBeacon==\"function\"?o.sendBeacon(e,new Blob([r],{type:\"application/json\"})):(n.fetch(e,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:r,keepalive:!0}),!0)}catch(r){return!1}}}function xn(e={}){var Me,ve;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",a=(Me=e.idleMs)!=null?Me:ne,s=(ve=e.sessionEveryMs)!=null?ve:3e4,u=e.slug||\"\",d=e.internal||null,l=e.sessionId||hn(),m=e.send||yn(o,n),p={},c=0,b=0,f=0,E=0,v=null,k=r(),R=!1,G=!1,N=[],Ge=()=>!!u||!!d,fe=i=>{if(!Ge())return!1;try{return m(i)!==!1}catch(g){return!1}},K=()=>!!t&&t.visibilityState===\"visible\"&&!R,ge=i=>{let g=r()-i,y=Math.max(0,k-i)+a;return Math.max(0,Math.min(g,y))},Y=()=>{v!=null&&c>0&&(p[c]=(p[c]||0)+ge(v)/1e3),v=K()?r():null},be=()=>{Y(),v=null},Z=()=>{v==null&&K()&&(v=r())},q=()=>{let i=0;for(let g in p)i+=p[g];return v!=null&&(i+=ge(v)/1e3),Math.round(i)},he=()=>{let i={};for(let g in p)i[g]=Math.round(p[g]);return i},A=()=>{k=r(),R&&(R=!1,Z())},ye=(i,g)=>{d||fe({slug:u,event:i,page:g||b,maxPage:f,seconds:q(),sessionId:l})},xe=\"\",O=()=>{Y();let i=q(),g=`${i}|${f}|${E}`;if(g===xe)return;let y={event:\"session\",sessionId:l,numPages:E,maxPage:f,totalSeconds:i,pagesTime:he()};d?(y.internal=!0,y.docId=d.docId,y.email=d.email,y.name=d.name,d.it&&(y.it=d.it)):y.slug=u,fe(y)&&(xe=g)},I=(i,g,y,we)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(g,y,we),N.push(()=>{i.removeEventListener(g,y,we)}))};return{sessionId:l,totalSeconds:q,maxPage:()=>f,pageTimes:he,start(){if(G)return;G=!0,ye(\"open\",1),c=1,k=r(),v=K()?r():null,I(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(be(),O()):(A(),Z())}),I(n,\"focus\",()=>{A(),Z()});for(let y of bn)I(n,y,A,{passive:!0});I(e.scrollElement,\"scroll\",A,{passive:!0}),I(n,\"beforeunload\",O);let i=n.setInterval(()=>{!R&&r()-k>a&&(R=!0,be())},5e3),g=n.setInterval(O,s);N.push(()=>{n.clearInterval(i),n.clearInterval(g)})},setPage(i){!i||i===b||(k=r(),Y(),c=i,b=i,i>f&&(f=i,ye(\"page\",i)))},setPageCount(i){E=i||0},noteActivity:A,flush:O,stop(){for(;N.length;){let i=N.pop();try{i==null||i()}catch(g){}}G=!1}}}var V={};w(V,{STORAGE_KEYS:()=>_,attendeeKey:()=>En,authorToken:()=>_n,avatarHtml:()=>oe,canModerate:()=>ue,createBudget:()=>Pn,createFileEcritures:()=>Cn,createScheduler:()=>An,escapeHtml:()=>x,fetchBorne:()=>In,flattenPresence:()=>Mn,formatMessageBody:()=>ae,initials:()=>$e,isMentioned:()=>Sn,isMine:()=>T,origineAvatarAutorisee:()=>De,presenceId:()=>Nn,reactorId:()=>se,referenceAuteur:()=>Tn,shouldNotify:()=>kn,unreadLabel:()=>Rn});var _={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\",presenceId:\"3dd-present-uid\"};function x(e){return String(e==null?\"\":e).replace(/&/g,\"&\").replace(/</g,\"<\").replace(/>/g,\">\").replace(/\"/g,\""\")}function $e(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function De(e,n){let t=String(e||\"\").trim();if(!t)return!1;if(/^[a-z][a-z0-9+.-]*:/i.test(t)===!1&&!t.startsWith(\"//\"))return!0;let r=typeof location!=\"undefined\"&&location?location.origin:\"\",o;try{o=new URL(t,r||\"https://invalide.exemple\")}catch(a){return!1}return o.protocol!==\"https:\"&&o.protocol!==\"http:\"?!1:r&&o.origin===r?!0:(n||[]).some(a=>{try{return new URL(String(a)).origin===o.origin}catch(s){return!1}})}function oe(e,n,t){return De(e,t)?`<img src=\"${x(e)}\" alt=\"\">`:x($e(n))}function ae(e){let n=x(e);return n=n.replace(/(https?:\\/\\/[^\\s<]+)/g,t=>`<a href=\"${t}\" target=_blank rel=noreferrer class=cm-link>${t}</a>`),n=n.replace(/(^|\\s)@([\\p{L}0-9_'.-]+)/gu,(t,r,o)=>`${r}<span class=cm-mention>@${o}</span>`),n}function Mn(e){let n=new Map;for(let t in e||{})for(let r of(e||{})[t]||[]){let o=String(r.uid||r.email||r.name||\"\").toLowerCase()||`_${t}_${n.size}`;(!n.get(o)||r.role===\"presenter\")&&n.set(o,r)}return[...n.values()]}function vn(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function wn(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function En(e,n=\"anon\",t=ie){try{if(!e)return`anon-${n}`;let r=e.getItem(_.attendeeKey);if(r)return r;let o=`anon-${t()}`;return e.setItem(_.attendeeKey,o),o}catch(r){return`anon-${n}`}}function ie(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(24));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}try{console.warn(\"[player] crypto indisponible : jeton d'auteur affaibli\")}catch(n){}return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}function _n(e,n=ie){let t=vn(e,_.authorToken);if(t)return t;let r=n();return wn(e,_.authorToken,r),r}async function Tn(e,n){let t=n!==void 0?n:typeof globalThis!=\"undefined\"&&globalThis.crypto?globalThis.crypto.subtle:null;if(!t||!e)return\"\";try{let r=async o=>{let a=await t.digest(\"SHA-256\",new TextEncoder().encode(o));return Array.from(new Uint8Array(a),s=>s.toString(16).padStart(2,\"0\")).join(\"\")};return(await r(\"ref:\"+await r(e))).slice(0,16)}catch(r){return\"\"}}function se(e){return String(e&&e.ref||\"\")}function T(e,n){return!e||!n||!e.author_ref||!n.ref?!1:e.author_ref===n.ref}function ue(e){return!!(e&&e.role===\"presenter\")}function Sn(e,n){if(!n||!n.name)return!1;let t=(n.name.split(/\\s+/)[0]||\"\").toLowerCase();return t?String(e&&e.body||\"\").toLowerCase().includes(`@${t}`)&&!T(e,n):!1}function kn(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||T(n,t)?!1:o}function Rn(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function An(e,n={}){var b;let t=(b=n.minMs)!=null?b:300,r=n.now||(()=>Date.now()),o=n.setTimer||((f,E)=>setTimeout(f,E)),a=n.clearTimer||(f=>clearTimeout(f)),s=!1,u=!1,d=-1/0,l=null,m=!1;function p(){if(m||s){u=!0;return}let f=t-(r()-d);if(f>0){c(f);return}s=!0,u=!1,d=r();let E=!1;e(()=>{E||(E=!0,s=!1,u&&!m&&p())})}function c(f){m||l!==null||(l=o(()=>{l=null,p()},Math.max(0,f)))}return{signaler(){m||p()},maintenant(){m||(d=-1/0,p())},arreter(){m=!0,l!==null&&(a(l),l=null)}}}function In(e,n,t=re,r={}){let o=r.fetch||(typeof fetch==\"function\"?fetch:void 0);if(!o)return Promise.reject(new Error(\"fetch indisponible\"));let a=r.setTimer||((c,b)=>setTimeout(c,b)),s=r.clearTimer||(c=>clearTimeout(c)),u=null;try{u=new AbortController}catch(c){}let d=u?Se(Q({},n||{}),{signal:u.signal}):Q({},n||{}),l=null,m=new Promise((c,b)=>{l=a(()=>{try{u==null||u.abort()}catch(f){}b(new Error(\"delai\"))},t)}),p=()=>{try{s(l)}catch(c){}};return Promise.race([o(e,d),m]).then(c=>(p(),c),c=>{throw p(),c})}function Pn(e){let n=e.now||(()=>Date.now()),t=Math.max(1,e.rafale),r=t,o=n();return{prendre(){let a=n();return r=Math.min(t,r+(a-o)*e.parHeure/36e5),o=a,r<1?!1:(r-=1,!0)},restants:()=>r}}function Cn(e={}){var l;let n=(l=e.minMs)!=null?l:0,t=e.now||(()=>Date.now()),r=e.setTimer||((m,p)=>setTimeout(m,p)),o=new Map,a=!1,s=-1/0,u=!1;function d(){if(a||o.size===0)return;let m=n-(t()-s);if(m>0){if(u)return;u=!0,r(()=>{u=!1,d()},m);return}let p=o.keys().next().value,c=o.get(p);o.delete(p),a=!0,s=t(),Promise.resolve().then(c.tache).then(b=>c.regler(b),()=>c.regler(null)).then(()=>{a=!1,d()})}return{poser(m,p){return new Promise(c=>{let b=o.get(m);o.set(m,{tache:p,regler:f=>{b&&b.regler(f),c(f)}}),d()})},enAttente:()=>o.size}}function Nn(e,n=ie){try{if(e){let t=e.getItem(_.presenceId);if(t)return t;let r=`p-${n()}`;return e.setItem(_.presenceId,r),r}}catch(t){}return`p-${n()}`}var F={};w(F,{messageClassName:()=>Ln,renderActions:()=>We,renderAttachment:()=>Fe,renderMessage:()=>On,renderReactions:()=>Ve});function Ve(e,n){let t=e&&e.reactions||{},r=se(n),o=\"\";for(let a in t){let s=t[a]||[];if(!s.length)continue;let u=s.indexOf(r)>=0;o+=`<button class=\"re-chip${u?\" on\":\"\"}\" data-e=\"${x(a)}\">${x(a)} ${s.length}</button>`}return o}function Fe(e){if(!e||!e.url)return\"\";let n=x(e.url);if(e.kind===\"image\")return`<a class=cm-att href=\"${n}\" target=_blank rel=noreferrer><img src=\"${n}\" alt=\"\" loading=lazy></a>`;let t=x(e.name||\"Document.pdf\");return`<a class=cm-att-pdf href=\"${n}\" target=_blank rel=noreferrer data-pdf=\"${n}\"><span class=cm-att-ph></span><span class=cm-pdflabel>${t}</span></a>`}function We(e,n){let t=T(e,n.me),r=`<button class=cm-react title=\"R\\xE9agir\">${n.reactIcon||\"\"}</button><button class=cm-reply title=\"R\\xE9pondre\">\\u21A9</button>`;return t&&(r+='<button class=cm-edit title=\"Modifier\">\\u270E</button>'),(t||ue(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function On(e,n){if(e.deleted)return'<span class=a></span><span class=b><div class=\"txt cm-del\">Message supprim\\xE9</div></span>';let t=e.reply_to?`<div class=cm-q><b>${x(e.reply_name||\"\")}</b> ${x(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${ae(e.body)}${r}</div>`:\"\",a=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${oe(e.author_avatar,e.author_name,n.avatarOrigins)}</span><span class=b><div class=who><b>${x(e.author_name||\"Invit\\xE9\")}</b>${a}</div>`+t+o+Fe(e.attachment)+`<div class=cm-re>${Ve(e,n.me)}</div></span><span class=cm-act>${We(e,n)}</span>`}function Ln(e,n,t){return\"cm\"+(T(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var W={};w(W,{DEFAULT_CENTER:()=>de,DEFAULT_ZOOM:()=>me,MAP_TYPES:()=>P,cycleMapType:()=>Hn,initialMapContent:()=>$n,mapTypeLabel:()=>Un,sanitizeContent:()=>pe});var P=[\"roadmap\",\"satellite\",\"hybrid\"],de=[46.6,2.5],me=6;function S(e){let n=Number(e);return Number.isFinite(n)?n:null}function le(e){if(!Array.isArray(e))return null;let n=S(e[0]),t=S(e[1]);return n!=null&&t!=null?[n,t]:null}var ce=(e,n,t)=>Math.max(n,Math.min(t,e));function pe(e){if(!e||typeof e!=\"object\")return null;let n=e;if(n.kind===\"pdf\"||n.kind==null)return null;if(n.kind===\"streetview\"){let t=le(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:S(r.heading)||0,pitch:ce(S(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:ce(S(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:le(n.center)||[...de],zoom:ce(Math.trunc(S(n.zoom)||me),1,21),marker:le(n.marker),mapType:P.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function Hn(e){let n=P.indexOf(e);return P[(n+1)%P.length]}function Un(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function $n(){return{kind:\"map\",center:[...de],zoom:me,marker:null,mapType:null,label:null}}var B={};w(B,{CURRENT_PAGE_MARGIN:()=>Bn,DEFAULT_ASPECT:()=>Fn,MAX_ZOOM:()=>Vn,MIN_PAGE_WIDTH:()=>Wn,MIN_ZOOM:()=>Dn,PRERENDER_MARGIN:()=>zn,ROTATIONS:()=>Xn,ancrageApresZoom:()=>Jn,arrowState:()=>Kn,aspectApresRotation:()=>je,averageColor:()=>qn,clampPage:()=>Be,clampZoom:()=>jn,fenetreVirtuelle:()=>nt,fitWidth:()=>Zn,isImageDocument:()=>Qn,pasVertical:()=>z,plafondFenetre:()=>tt,positionDe:()=>et,progressPercent:()=>Gn,rotationEffective:()=>C});var Dn=.5,Vn=3,Fn=1.35,Wn=280,zn=\"500px 0px\",Bn=\"-48% 0px -48% 0px\";function jn(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function Be(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function Gn(e,n){return n?Math.round(Be(e,n)/n*100):0}function Kn(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var Yn=28;function Zn(e){var s;let n=(s=e.zoom)!=null?s:1,t=Math.max(280,e.containerWidth||900)*n;if(!e.onePage)return t;let r=je(e.aspect,e.rotation),a=((e.containerHeight||600)-Yn-(e.overlap||0)-(e.reserve||0))/r;return a>120?Math.min(t,a):t}function qn(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let a=0;a<e.length;a+=4)t+=e[a],r+=e[a+1],o+=e[a+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function Qn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var Xn=[0,90,180,270],h=(e,n=0)=>typeof e==\"number\"&&Number.isFinite(e)?e:n;function C(e,n){return(Math.round((h(e)+h(n))/90)%4+4)%4*90}function je(e,n){let t=h(e,0)>0?e:1.35,r=C(0,n);return r===90||r===270?1/t:t}function ze(e,n,t,r,o,a,s){let u=Math.min(Math.max(0,o),Math.max(0,r)),d=u+Math.max(0,r-u)*s,l=Math.min(Math.max(0,a),Math.max(0,r)),m=r-l>0?(d-l)/(r-l):1,p=Math.max(0,(t-r)/2),c=n+e-p,b=l+Math.max(0,c-l)*m-n;return Math.min(Math.max(0,b),Math.max(0,d-t))}function Jn(e){let n=h(e.zoomAvant,0),t=h(e.zoomApres,0),r={x:h(e.defilement&&e.defilement.x),y:h(e.defilement&&e.defilement.y)};if(n<=0||t<=0)return r;let o=t/n,a=e.fixe||{largeur:0,hauteur:0},s=e.fixeDebut||{largeur:0,hauteur:0};return{x:ze(r.x,h(e.point&&e.point.x),h(e.cadre&&e.cadre.largeur),h(e.contenu&&e.contenu.largeur),h(a.largeur),h(s.largeur),o),y:ze(r.y,h(e.point&&e.point.y),h(e.cadre&&e.cadre.hauteur),h(e.contenu&&e.contenu.hauteur),h(a.hauteur),h(s.hauteur),o)}}var M=(e,n=0)=>{let t=Number(e);return Number.isFinite(t)?t:n};function z(e,n){return Math.max(1,M(e,1))+Math.max(0,M(n))}function et(e,n){return Math.max(0,M(n.decalageHaut))+(Math.max(1,Math.trunc(M(e,1)))-1)*z(n.hauteurElement,n.ecart)}function nt(e){let n=Math.max(0,Math.trunc(M(e.total)));if(n===0)return{debut:0,fin:0,avant:0,apres:0};let t=z(e.hauteurElement,e.ecart),r=Math.max(0,Math.trunc(M(e.marge))),o=Math.max(0,M(e.decalageHaut)),a=Math.max(0,M(e.debutVisible))-o,s=a+Math.max(0,M(e.hauteurVisible)),u=Math.floor(a/t)+1,d=Math.floor(s/t)+1,l=Math.min(n,Math.max(1,u-r)),m=Math.max(l,Math.min(n,d+r));return{debut:l,fin:m,avant:(l-1)*t,apres:(n-m)*t}}function tt(e){let n=z(e.hauteurElement,e.ecart);return Math.ceil(Math.max(0,M(e.hauteurVisible))/n)+2+2*Math.max(0,Math.trunc(M(e.marge)))}var j={};w(j,{presentationTransition:()=>rt,switchDocUrl:()=>ot});function rt(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=pe(e.content);if(t)return[{kind:\"show-map\",content:t}];let r=[{kind:\"leave-map\"}],o=C(0,e.view_rotation);return o!==C(0,n.rotation)&&r.push({kind:\"rotate\",rotation:o}),e.file_url&&e.file_url!==n.docUrl?(r.push({kind:\"switch-doc\",url:e.file_url,name:e.file_name||void 0,title:e.doc_title||void 0,updatedAt:e.updated_at||void 0}),r):(e.current_page&&r.push({kind:\"show-page\",page:e.current_page}),r)}function ot(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return Je(at);})();", SOURCE_HASH: "ac174cbf0b8d7e02" };
|
|
9
|
+
module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var H=Object.defineProperty,Ye=Object.defineProperties,Ze=Object.getOwnPropertyDescriptor,qe=Object.getOwnPropertyDescriptors,Qe=Object.getOwnPropertyNames,_e=Object.getOwnPropertySymbols;var Se=Object.prototype.hasOwnProperty,Xe=Object.prototype.propertyIsEnumerable;var Te=(e,n,t)=>n in e?H(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t,Q=(e,n)=>{for(var t in n||(n={}))Se.call(n,t)&&Te(e,t,n[t]);if(_e)for(var t of _e(n))Xe.call(n,t)&&Te(e,t,n[t]);return e},ke=(e,n)=>Ye(e,qe(n));var E=(e,n)=>{for(var t in n)H(e,t,{get:n[t],enumerable:!0})},Je=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of Qe(n))!Se.call(e,o)&&o!==t&&H(e,o,{get:()=>n[o],enumerable:!(r=Ze(n,o))||r.enumerable});return e};var en=e=>Je(H({},\"__esModule\",{value:!0}),e);var it={};E(it,{bridge:()=>U,cadence:()=>D,chat:()=>W,live:()=>V,presentation:()=>z,presentationState:()=>j,tracking:()=>F,viewer:()=>B});var U={};E(U,{onHostMessage:()=>dn,onPlayerMessage:()=>cn,parseHostMessage:()=>Ie,parsePlayerMessage:()=>Ae,sendToHost:()=>sn,sendToPlayer:()=>un,toWire:()=>J});var X=\"3dd-doc-\",nn=/^[A-Za-z0-9_-]{1,64}$/,tn=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],rn=[\"embed-denied\"],on=[\"present-invite\",\"present-handover\",\"present-switch\"],an=[\"handover-done\"];function J(e){let n={type:X+e.type};return\"slug\"in e&&e.slug&&(n.slug=e.slug),\"reason\"in e&&e.reason&&(n.reason=String(e.reason).slice(0,40)),n}function Re(e,n,t){if(!e||typeof e!=\"object\")return null;let r=e.type;if(typeof r!=\"string\"||!r.startsWith(X))return null;let o=r.slice(X.length);if(n.includes(o))return{type:o};if(rn.includes(o)){let s=e.reason;return{type:o,reason:/^[a-z-]{1,40}$/.test(String(s||\"\"))?String(s):\"unknown\"}}if(!t.includes(o))return null;let a=e.slug;return typeof a!=\"string\"||!nn.test(a)?null:{type:o,slug:a}}function Ae(e){return Re(e,tn,on)}function Ie(e){return Re(e,an,[])}function sn(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(J(e),\"*\")}catch(t){}}function un(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(J(n),\"*\")}catch(r){}}function ln(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function cn(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=a=>{if(!ln(t,a.source))return;let s=Ae(a.data);s&&e(s)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function dn(e,n){var a;let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=(a=t.parent)!=null?a:null,o=s=>{if(!r||r===t||s.source!==r)return;let u=Ie(s.data);u&&e(u)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var F={};E(F,{createTracker:()=>Mn});var D={};E(D,{PRESENTER_ACTIONS_PER_HOUR:()=>te,PRESENT_CACHE_MS:()=>gn,PRESENT_QUOTA_MARGIN:()=>$e,PRESENT_QUOTA_PER_HOUR:()=>bn,PRESENT_READS_PER_HOUR:()=>Le,PRESENT_READ_BURST:()=>He,PRESENT_READ_COALESCE_MS:()=>Ue,PRESENT_RESYNC_MS:()=>Ce,PRESENT_SIGNAL_BUDGET_PER_HOUR:()=>fn,PRESENT_WRITE_TIMEOUT_MS:()=>re,READERS_PER_EGRESS:()=>$,RESYNC_READS_PER_HOUR:()=>Oe,SESSION_IDLE_MS:()=>ne,SESSION_INTERVAL_MS:()=>ee,SESSION_QUOTA_PER_HOUR:()=>mn,SESSION_WRITES_PER_HOUR:()=>Pe,VIEW_EVENTS_PER_READER_HOUR:()=>Ne,VIEW_QUOTA_PER_HOUR:()=>pn});var ee=3e4,Pe=Math.ceil(120),$=25,mn=Pe*$,Ne=720,pn=Ne*$,ne=18e4,Ce=25e3,Oe=Math.ceil(36e5/Ce),te=720,Le=Oe+te,fn=te,He=20,re=1e4,Ue=400,gn=Ue,$e=4,bn=(Le+He)*$*$e;var hn=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function yn(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(16));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}return Math.random().toString(36).slice(2)+Date.now().toString(36)}function xn(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;return o&&typeof o.sendBeacon==\"function\"?o.sendBeacon(e,new Blob([r],{type:\"application/json\"})):(n.fetch(e,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:r,keepalive:!0}),!0)}catch(r){return!1}}}function Mn(e={}){var ve,Ee;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",a=(ve=e.idleMs)!=null?ve:ne,s=(Ee=e.sessionEveryMs)!=null?Ee:3e4,u=e.slug||\"\",d=e.internal||null,l=e.sessionId||yn(),m=e.send||xn(o,n),p={},c=0,b=0,f=0,w=0,v=null,k=r(),R=!1,G=!1,O=[],Ke=()=>!!u||!!d,ge=i=>{if(!Ke())return!1;try{return m(i)!==!1}catch(g){return!1}},K=()=>!!t&&t.visibilityState===\"visible\"&&!R,be=i=>{let g=r()-i,x=Math.max(0,k-i)+a;return Math.max(0,Math.min(g,x))},Y=()=>{v!=null&&c>0&&(p[c]=(p[c]||0)+be(v)/1e3),v=K()?r():null},he=()=>{Y(),v=null},Z=()=>{v==null&&K()&&(v=r())},q=()=>{let i=0;for(let g in p)i+=p[g];return v!=null&&(i+=be(v)/1e3),Math.round(i)},ye=()=>{let i={};for(let g in p)i[g]=Math.round(p[g]);return i},A=()=>{k=r(),R&&(R=!1,Z())},xe=(i,g)=>{d||ge({slug:u,event:i,page:g||b,maxPage:f,seconds:q(),sessionId:l})},Me=\"\",L=()=>{Y();let i=q(),g=`${i}|${f}|${w}`;if(g===Me)return;let x={event:\"session\",sessionId:l,numPages:w,maxPage:f,totalSeconds:i,pagesTime:ye()};d?(x.internal=!0,x.docId=d.docId,x.email=d.email,x.name=d.name,d.it&&(x.it=d.it)):x.slug=u,ge(x)&&(Me=g)},I=(i,g,x,we)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(g,x,we),O.push(()=>{i.removeEventListener(g,x,we)}))};return{sessionId:l,totalSeconds:q,maxPage:()=>f,pageTimes:ye,start(){if(G)return;G=!0,xe(\"open\",1),c=1,k=r(),v=K()?r():null,I(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(he(),L()):(A(),Z())}),I(n,\"focus\",()=>{A(),Z()});for(let x of hn)I(n,x,A,{passive:!0});I(e.scrollElement,\"scroll\",A,{passive:!0}),I(n,\"beforeunload\",L);let i=n.setInterval(()=>{!R&&r()-k>a&&(R=!0,he())},5e3),g=n.setInterval(L,s);O.push(()=>{n.clearInterval(i),n.clearInterval(g)})},setPage(i){!i||i===b||(k=r(),Y(),c=i,b=i,i>f&&(f=i,xe(\"page\",i)))},setPageCount(i){w=i||0},noteActivity:A,flush:L,stop(){for(;O.length;){let i=O.pop();try{i==null||i()}catch(g){}}G=!1}}}var V={};E(V,{STORAGE_KEYS:()=>_,attendeeKey:()=>_n,authorToken:()=>Tn,avatarHtml:()=>oe,canModerate:()=>ue,createBudget:()=>Nn,createFileEcritures:()=>Cn,createScheduler:()=>In,escapeHtml:()=>M,fetchBorne:()=>Pn,flattenPresence:()=>vn,formatMessageBody:()=>ae,initials:()=>De,isMentioned:()=>kn,isMine:()=>T,origineAvatarAutorisee:()=>Fe,presenceId:()=>On,reactorId:()=>ie,referenceAuteur:()=>Sn,shouldNotify:()=>Rn,unreadLabel:()=>An});var _={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\",presenceId:\"3dd-present-uid\"};function M(e){return String(e==null?\"\":e).replace(/&/g,\"&\").replace(/</g,\"<\").replace(/>/g,\">\").replace(/\"/g,\""\")}function De(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function Fe(e,n){let t=String(e||\"\").trim();if(!t)return!1;if(/^[a-z][a-z0-9+.-]*:/i.test(t)===!1&&!t.startsWith(\"//\"))return!0;let r=typeof location!=\"undefined\"&&location?location.origin:\"\",o;try{o=new URL(t,r||\"https://invalide.exemple\")}catch(a){return!1}return o.protocol!==\"https:\"&&o.protocol!==\"http:\"?!1:r&&o.origin===r?!0:(n||[]).some(a=>{try{return new URL(String(a)).origin===o.origin}catch(s){return!1}})}function oe(e,n,t){return Fe(e,t)?`<img src=\"${M(e)}\" alt=\"\">`:M(De(n))}function ae(e){let n=M(e);return n=n.replace(/(https?:\\/\\/[^\\s<]+)/g,t=>`<a href=\"${t}\" target=_blank rel=noreferrer class=cm-link>${t}</a>`),n=n.replace(/(^|\\s)@([\\p{L}0-9_'.-]+)/gu,(t,r,o)=>`${r}<span class=cm-mention>@${o}</span>`),n}function vn(e){let n=new Map;for(let t in e||{})for(let r of(e||{})[t]||[]){let o=String(r.uid||r.email||r.name||\"\").toLowerCase()||`_${t}_${n.size}`;(!n.get(o)||r.role===\"presenter\")&&n.set(o,r)}return[...n.values()]}function En(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function wn(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function _n(e,n=\"anon\",t=se){try{if(!e)return`anon-${n}`;let r=e.getItem(_.attendeeKey);if(r)return r;let o=`anon-${t()}`;return e.setItem(_.attendeeKey,o),o}catch(r){return`anon-${n}`}}function se(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(24));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}try{console.warn(\"[player] crypto indisponible : jeton d'auteur affaibli\")}catch(n){}return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}function Tn(e,n=se){let t=En(e,_.authorToken);if(t)return t;let r=n();return wn(e,_.authorToken,r),r}async function Sn(e,n){let t=n!==void 0?n:typeof globalThis!=\"undefined\"&&globalThis.crypto?globalThis.crypto.subtle:null;if(!t||!e)return\"\";try{let r=async o=>{let a=await t.digest(\"SHA-256\",new TextEncoder().encode(o));return Array.from(new Uint8Array(a),s=>s.toString(16).padStart(2,\"0\")).join(\"\")};return(await r(\"ref:\"+await r(e))).slice(0,16)}catch(r){return\"\"}}function ie(e){return String(e&&e.ref||\"\")}function T(e,n){return!e||!n||!e.author_ref||!n.ref?!1:e.author_ref===n.ref}function ue(e){return!!(e&&e.role===\"presenter\")}function kn(e,n){if(!n||!n.name)return!1;let t=(n.name.split(/\\s+/)[0]||\"\").toLowerCase();return t?String(e&&e.body||\"\").toLowerCase().includes(`@${t}`)&&!T(e,n):!1}function Rn(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||T(n,t)?!1:o}function An(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function In(e,n={}){var b;let t=(b=n.minMs)!=null?b:300,r=n.now||(()=>Date.now()),o=n.setTimer||((f,w)=>setTimeout(f,w)),a=n.clearTimer||(f=>clearTimeout(f)),s=!1,u=!1,d=-1/0,l=null,m=!1;function p(){if(m||s){u=!0;return}let f=t-(r()-d);if(f>0){c(f);return}s=!0,u=!1,d=r();let w=!1;e(()=>{w||(w=!0,s=!1,u&&!m&&p())})}function c(f){m||l!==null||(l=o(()=>{l=null,p()},Math.max(0,f)))}return{signaler(){m||p()},maintenant(){m||(d=-1/0,p())},arreter(){m=!0,l!==null&&(a(l),l=null)}}}function Pn(e,n,t=re,r={}){let o=r.fetch||(typeof fetch==\"function\"?fetch:void 0);if(!o)return Promise.reject(new Error(\"fetch indisponible\"));let a=r.setTimer||((c,b)=>setTimeout(c,b)),s=r.clearTimer||(c=>clearTimeout(c)),u=null;try{u=new AbortController}catch(c){}let d=u?ke(Q({},n||{}),{signal:u.signal}):Q({},n||{}),l=null,m=new Promise((c,b)=>{l=a(()=>{try{u==null||u.abort()}catch(f){}b(new Error(\"delai\"))},t)}),p=()=>{try{s(l)}catch(c){}};return Promise.race([o(e,d),m]).then(c=>(p(),c),c=>{throw p(),c})}function Nn(e){let n=e.now||(()=>Date.now()),t=Math.max(1,e.rafale),r=t,o=n();return{prendre(){let a=n();return r=Math.min(t,r+(a-o)*e.parHeure/36e5),o=a,r<1?!1:(r-=1,!0)},restants:()=>r}}function Cn(e={}){var l;let n=(l=e.minMs)!=null?l:0,t=e.now||(()=>Date.now()),r=e.setTimer||((m,p)=>setTimeout(m,p)),o=new Map,a=!1,s=-1/0,u=!1;function d(){if(a||o.size===0)return;let m=n-(t()-s);if(m>0){if(u)return;u=!0,r(()=>{u=!1,d()},m);return}let p=o.keys().next().value,c=o.get(p);o.delete(p),a=!0,s=t(),Promise.resolve().then(c.tache).then(b=>c.regler(b),()=>c.regler(null)).then(()=>{a=!1,d()})}return{poser(m,p){return new Promise(c=>{let b=o.get(m);o.set(m,{tache:p,regler:f=>{b&&b.regler(f),c(f)}}),d()})},enAttente:()=>o.size}}function On(e,n=se){try{if(e){let t=e.getItem(_.presenceId);if(t)return t;let r=`p-${n()}`;return e.setItem(_.presenceId,r),r}}catch(t){}return`p-${n()}`}var W={};E(W,{messageClassName:()=>Hn,renderActions:()=>ze,renderAttachment:()=>We,renderMessage:()=>Ln,renderReactions:()=>Ve});function Ve(e,n){let t=e&&e.reactions||{},r=ie(n),o=\"\";for(let a in t){let s=t[a]||[];if(!s.length)continue;let u=s.indexOf(r)>=0;o+=`<button class=\"re-chip${u?\" on\":\"\"}\" data-e=\"${M(a)}\">${M(a)} ${s.length}</button>`}return o}function We(e){if(!e||!e.url)return\"\";let n=M(e.url);if(e.kind===\"image\")return`<a class=cm-att href=\"${n}\" target=_blank rel=noreferrer><img src=\"${n}\" alt=\"\" loading=lazy></a>`;let t=M(e.name||\"Document.pdf\");return`<a class=cm-att-pdf href=\"${n}\" target=_blank rel=noreferrer data-pdf=\"${n}\"><span class=cm-att-ph></span><span class=cm-pdflabel>${t}</span></a>`}function ze(e,n){let t=T(e,n.me),r=`<button class=cm-react title=\"R\\xE9agir\">${n.reactIcon||\"\"}</button><button class=cm-reply title=\"R\\xE9pondre\">\\u21A9</button>`;return t&&(r+='<button class=cm-edit title=\"Modifier\">\\u270E</button>'),(t||ue(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function Ln(e,n){if(e.deleted)return'<span class=a></span><span class=b><div class=\"txt cm-del\">Message supprim\\xE9</div></span>';let t=e.reply_to?`<div class=cm-q><b>${M(e.reply_name||\"\")}</b> ${M(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${ae(e.body)}${r}</div>`:\"\",a=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${oe(e.author_avatar,e.author_name,n.avatarOrigins)}</span><span class=b><div class=who><b>${M(e.author_name||\"Invit\\xE9\")}</b>${a}</div>`+t+o+We(e.attachment)+`<div class=cm-re>${Ve(e,n.me)}</div></span><span class=cm-act>${ze(e,n)}</span>`}function Hn(e,n,t){return\"cm\"+(T(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var z={};E(z,{DEFAULT_CENTER:()=>de,DEFAULT_ZOOM:()=>me,MAP_TYPES:()=>P,cycleMapType:()=>Un,initialMapContent:()=>Dn,mapTypeLabel:()=>$n,sanitizeContent:()=>pe});var P=[\"roadmap\",\"satellite\",\"hybrid\"],de=[46.6,2.5],me=6;function S(e){let n=Number(e);return Number.isFinite(n)?n:null}function le(e){if(!Array.isArray(e))return null;let n=S(e[0]),t=S(e[1]);return n!=null&&t!=null?[n,t]:null}var ce=(e,n,t)=>Math.max(n,Math.min(t,e));function pe(e){if(!e||typeof e!=\"object\")return null;let n=e;if(n.kind===\"pdf\"||n.kind==null)return null;if(n.kind===\"streetview\"){let t=le(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:S(r.heading)||0,pitch:ce(S(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:ce(S(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:le(n.center)||[...de],zoom:ce(Math.trunc(S(n.zoom)||me),1,21),marker:le(n.marker),mapType:P.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function Un(e){let n=P.indexOf(e);return P[(n+1)%P.length]}function $n(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function Dn(){return{kind:\"map\",center:[...de],zoom:me,marker:null,mapType:null,label:null}}var B={};E(B,{CURRENT_PAGE_MARGIN:()=>jn,DEFAULT_ASPECT:()=>Wn,MAX_ZOOM:()=>Vn,MIN_PAGE_WIDTH:()=>zn,MIN_ZOOM:()=>Fn,PLAFOND_DEFILEMENT_PX:()=>fe,PRERENDER_MARGIN:()=>Bn,ROTATIONS:()=>Jn,ancrageApresZoom:()=>et,arrowState:()=>Yn,aspectApresRotation:()=>Ge,averageColor:()=>Qn,clampPage:()=>je,clampZoom:()=>Gn,fenetreVirtuelle:()=>tt,fitWidth:()=>qn,isImageDocument:()=>Xn,pagesAtteignables:()=>ot,pasVertical:()=>C,plafondFenetre:()=>rt,positionDe:()=>nt,progressPercent:()=>Kn,rotationEffective:()=>N});var Fn=.5,Vn=3,Wn=1.35,zn=280,Bn=\"500px 0px\",jn=\"-48% 0px -48% 0px\";function Gn(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function je(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function Kn(e,n){return n?Math.round(je(e,n)/n*100):0}function Yn(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var Zn=28;function qn(e){var s;let n=(s=e.zoom)!=null?s:1,t=Math.max(280,e.containerWidth||900)*n;if(!e.onePage)return t;let r=Ge(e.aspect,e.rotation),a=((e.containerHeight||600)-Zn-(e.overlap||0)-(e.reserve||0))/r;return a>120?Math.min(t,a):t}function Qn(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let a=0;a<e.length;a+=4)t+=e[a],r+=e[a+1],o+=e[a+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function Xn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var Jn=[0,90,180,270],h=(e,n=0)=>typeof e==\"number\"&&Number.isFinite(e)?e:n;function N(e,n){return(Math.round((h(e)+h(n))/90)%4+4)%4*90}function Ge(e,n){let t=h(e,0)>0?e:1.35,r=N(0,n);return r===90||r===270?1/t:t}function Be(e,n,t,r,o,a,s){let u=Math.min(Math.max(0,o),Math.max(0,r)),d=u+Math.max(0,r-u)*s,l=Math.min(Math.max(0,a),Math.max(0,r)),m=r-l>0?(d-l)/(r-l):1,p=Math.max(0,(t-r)/2),c=n+e-p,b=l+Math.max(0,c-l)*m-n;return Math.min(Math.max(0,b),Math.max(0,d-t))}function et(e){let n=h(e.zoomAvant,0),t=h(e.zoomApres,0),r={x:h(e.defilement&&e.defilement.x),y:h(e.defilement&&e.defilement.y)};if(n<=0||t<=0)return r;let o=t/n,a=e.fixe||{largeur:0,hauteur:0},s=e.fixeDebut||{largeur:0,hauteur:0};return{x:Be(r.x,h(e.point&&e.point.x),h(e.cadre&&e.cadre.largeur),h(e.contenu&&e.contenu.largeur),h(a.largeur),h(s.largeur),o),y:Be(r.y,h(e.point&&e.point.y),h(e.cadre&&e.cadre.hauteur),h(e.contenu&&e.contenu.hauteur),h(a.hauteur),h(s.hauteur),o)}}var y=(e,n=0)=>{let t=Number(e);return Number.isFinite(t)?t:n};function C(e,n){return Math.max(1,y(e,1))+Math.max(0,y(n))}function nt(e,n){return Math.max(0,y(n.decalageHaut))+(Math.max(1,Math.trunc(y(e,1)))-1)*C(n.hauteurElement,n.ecart)}function tt(e){let n=Math.max(0,Math.trunc(y(e.total)));if(n===0)return{debut:0,fin:0,avant:0,apres:0};let t=C(e.hauteurElement,e.ecart),r=Math.max(0,Math.trunc(y(e.marge))),o=Math.max(0,y(e.decalageHaut)),a=Math.max(0,y(e.debutVisible))-o,s=a+Math.max(0,y(e.hauteurVisible)),u=Math.floor(a/t)+1,d=Math.floor(s/t)+1,l=Math.min(n,Math.max(1,u-r)),m=Math.max(l,Math.min(n,d+r));return{debut:l,fin:m,avant:(l-1)*t,apres:(n-m)*t}}function rt(e){let n=C(e.hauteurElement,e.ecart);return Math.ceil(Math.max(0,y(e.hauteurVisible))/n)+2+2*Math.max(0,Math.trunc(y(e.marge)))}var fe=33554428;function ot(e,n=fe){let t=Math.max(0,Math.trunc(y(e.total)));if(t===0)return 0;let r=C(e.hauteurElement,e.ecart),o=Math.max(1,y(e.hauteurElement,1)),a=Math.max(0,y(e.decalageHaut)),s=Math.max(0,y(n,fe))-a-o;return s<0?1:Math.max(1,Math.min(t,Math.floor(s/r)+1))}var j={};E(j,{presentationTransition:()=>at,switchDocUrl:()=>st});function at(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=pe(e.content);if(t)return[{kind:\"show-map\",content:t}];let r=[{kind:\"leave-map\"}],o=N(0,e.view_rotation);return o!==N(0,n.rotation)&&r.push({kind:\"rotate\",rotation:o}),e.file_url&&e.file_url!==n.docUrl?(r.push({kind:\"switch-doc\",url:e.file_url,name:e.file_name||void 0,title:e.doc_title||void 0,updatedAt:e.updated_at||void 0}),r):(e.current_page&&r.push({kind:\"show-page\",page:e.current_page}),r)}function st(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return en(it);})();", SOURCE_HASH: "60dc161c7dc11943" };
|
package/server/gabarit-agent.js
CHANGED
|
@@ -102,6 +102,7 @@ const BOT_CSS = `
|
|
|
102
102
|
body.onepage #pages{height:100%;display:flex;align-items:center;justify-content:center;padding:0}
|
|
103
103
|
|
|
104
104
|
body.onepage #pages .page{display:none;margin:0}
|
|
105
|
+
body.onepage #pages .pspace{display:none} /* un espaceur de pages absentes pousserait la page courante hors de l'écran */
|
|
105
106
|
body.onepage #pages .page.cur{display:block;box-shadow:0 6px 34px rgba(0,0,0,.16)}
|
|
106
107
|
/* Transition de page sobre en mode guidé/lecture (glissé directionnel ~0,28 s). Exclut le player mobile
|
|
107
108
|
(rythme rapide). Activable/désactivable via .botanim (CFG.botAnim, défaut ON — param profil futur). */
|
package/server/gabarit-live.js
CHANGED
|
@@ -595,11 +595,13 @@ var Live=(function(){
|
|
|
595
595
|
// C'est une meilleure propriété que d'essayer de l'empêcher — elle vaut aussi le jour où le
|
|
596
596
|
// transport lui-même a un défaut.
|
|
597
597
|
//
|
|
598
|
-
// ⚠️
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
// la fin de la présentation — passe par 'state', qui est relu.
|
|
598
|
+
// ⚠️ INVARIANT : AUCUNE CHARGE DE DIFFUSION N'EST APPLIQUÉE TELLE QUELLE. 'map' déclenche une
|
|
599
|
+
// relecture de l'état serveur SANS lire sa charge (voir plus bas, « aucune charge ne passe »).
|
|
600
|
+
// Seul 'typing' reste volontairement éphémère et non confronté : « untel écrit » n'a pas d'état
|
|
601
|
+
// serveur et ne porte aucun privilège — au pire un faux « untel écrit ». Ce qui fait autorité —
|
|
602
|
+
// la page affichée, le document, la fin de la présentation — passe par 'state', qui est relu.
|
|
603
|
+
// (Ce paragraphe affirmait que 'map' restait appliqué tel quel alors que le code l'ignorait
|
|
604
|
+
// déjà : réécrit le 13/09 sur relevé d'un audit externe.)
|
|
603
605
|
function relire(url,applique){
|
|
604
606
|
fetch('/api/doc?present='+encodeURIComponent(SLUG)+url)
|
|
605
607
|
.then(function(r){return r.json();})
|
|
@@ -669,16 +671,23 @@ var Live=(function(){
|
|
|
669
671
|
// rattrape ce cas : elle ne coûte presque rien et évite qu'une audience reste figée sur un
|
|
670
672
|
// état périmé en croyant être à jour. C'est l'inverse d'une optimisation : c'est le prix de
|
|
671
673
|
// la borne.
|
|
672
|
-
|
|
674
|
+
// ⚠️ ET LE FILET NE PART PAS EN CHŒUR. Mille spectateurs qui rejoignent ensemble relisaient
|
|
675
|
+
// ensemble : une rafale de 2 000 GET toutes les 25 s, en phase, pour une audience qui ne fait
|
|
676
|
+
// rien. Un décalage initial tiré entre 0 et 25 s étale la rafale sans augmenter la fréquence
|
|
677
|
+
// maximale — chacun garde exactement une relecture de chaque point par 25 s. Le même
|
|
678
|
+
// identifiant porte d'abord le délai puis l'intervalle : clearInterval les retire tous deux.
|
|
679
|
+
function filetTick(){_ordEtat.maintenant();_ordChat.maintenant();}
|
|
680
|
+
_filet=setTimeout(function(){filetTick();_filet=setInterval(filetTick,25000);},Math.floor(Math.random()*25000));
|
|
673
681
|
|
|
674
|
-
// ⚠️ LE TITRE
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
+
// ⚠️ INVARIANT : LE TITRE « PRÉSENTÉ PAR » VIENT EXCLUSIVEMENT DE state.presenter_name, relu
|
|
683
|
+
// auprès du serveur — jamais de la présence Realtime, que chacun compose lui-même. Aucune
|
|
684
|
+
// métadonnée de présence (role, name, avatar) ne porte un privilège : la liste des participants
|
|
685
|
+
// n'affiche aucun badge de rôle, et modération comme paternité sont arbitrées par le serveur
|
|
686
|
+
// sur ses propres jetons. Une présence hostile peut au plus falsifier SA ligne cosmétique.
|
|
687
|
+
// Raison : le canal ne peut pas arbitrer — un participant légitime a le droit d'y écrire SA
|
|
688
|
+
// présence, donc rien de ce qui y transite ne peut fonder un droit.
|
|
689
|
+
// (Ce commentaire affirmait que « le serveur renvoie la clé et l'audience compare » : cette clé
|
|
690
|
+
// n'existe plus, le serveur ne rend qu'un nom. Réécrit le 13/09 sur relevé d'un audit externe.)
|
|
682
691
|
function etatDuServeur(st){
|
|
683
692
|
if(typeof st.presenter_name!=='undefined'){var n2=st.presenter_name||'';
|
|
684
693
|
if(n2!==PRESNAME){PRESNAME=n2;try{if(ch)renderPres(ch.presenceState());}catch(e){}}}
|
package/server/handler.js
CHANGED
|
@@ -40,6 +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
|
+
// ⚠️ 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
|
+
}
|
|
43
59
|
// Le domaine reçoit le même contexte : une seule construction pour tout le player.
|
|
44
60
|
require("./shares").init(ctx);
|
|
45
61
|
require("./retention").init(ctx);
|
|
@@ -55,6 +71,55 @@ function init(ctx) {
|
|
|
55
71
|
}
|
|
56
72
|
const isAllowedStorageUrl = (url) => PLAYER.storage.isAllowedUrl(url);
|
|
57
73
|
|
|
74
|
+
// ⚠️ LE FLUX BORNAIT LES OCTETS, RIEN NE BORNAIT LE NOMBRE DE FLUX. `fetchFile` partait sans
|
|
75
|
+
// admission : 200 demandes lentes simultanées → 200 connexions amont, 200 pipelines, 200 réponses
|
|
76
|
+
// ouvertes, dans un seul processus (reproduit par un audit externe le 13/09). Le streaming empêche
|
|
77
|
+
// d'allouer 60 Mo par requête ; il n'empêche pas d'ouvrir mille sockets. Une admission par processus,
|
|
78
|
+
// AVANT l'appel amont : refus immédiat 503 + Retry-After quand c'est plein — aucune file d'attente,
|
|
79
|
+
// une file non bornée est le même défaut avec un délai — et la place rendue dans un `finally`, donc
|
|
80
|
+
// aussi sur erreur amont et sur déconnexion cliente (le pipeline rejette alors). Le plafond vient du
|
|
81
|
+
// contexte (`config.maxConcurrentRelays`, défaut 64) : assez pour les requêtes Range parallèles de
|
|
82
|
+
// pdf.js, borné pour un processus. Sur serverless la plate-forme borne déjà la concurrence globale ;
|
|
83
|
+
// ceci protège le mode autonome et chaque instance chaude.
|
|
84
|
+
const { bornesRelais, RELAIS_SIMULTANES_DEFAUT, RELAIS_STALL_MS_DEFAUT, RELAIS_MAX_MS_DEFAUT } = require("./bornes");
|
|
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;
|
|
93
|
+
// ⚠️ UNE PLACE N'EST BORNÉE QUE SI LE RELAIS QUI L'OCCUPE FINIT. Un client qui cesse de lire — ou un
|
|
94
|
+
// amont qui cesse d'envoyer — laissait le pipeline en attente pour toujours : `finally` jamais atteint,
|
|
95
|
+
// place jamais rendue, et avec un plafond de 1, plus aucun fichier ne partait (reproduit par un audit
|
|
96
|
+
// externe le 13/09). `requestTimeout` ne borne que la RÉCEPTION de la requête, pas l'émission de la
|
|
97
|
+
// réponse — le commentaire de `bin/serve.js` affirmait le contraire. Deux bornes, configurables par
|
|
98
|
+
// le contexte : sans progression pendant `relayStallMs` (30 s), ou au-delà de `relayMaxMs` (15 min),
|
|
99
|
+
// le pipeline est ABANDONNÉ par signal — source amont détruite, réponse détruite, rejet, `finally`.
|
|
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).
|
|
102
|
+
let relaisStallMs = RELAIS_STALL_MS_DEFAUT, relaisMaxMs = RELAIS_MAX_MS_DEFAUT;
|
|
103
|
+
async function relayerSousAdmission(res, travail) {
|
|
104
|
+
if (relaisEnCours >= plafondRelais) {
|
|
105
|
+
relaisRefusesTotal += 1; dernierRefusRelais = Date.now();
|
|
106
|
+
// Une fois par heure, l'exploitant l'apprend : un 503 muet ressemble à une panne d'amont.
|
|
107
|
+
try {
|
|
108
|
+
if (await PLAYER.limits.allow("relais:sature-avert", 1, 3600)) {
|
|
109
|
+
PLAYER.errors.capture(new Error(`relais refusés : ${plafondRelais} transferts simultanés atteints dans ce processus (config.maxConcurrentRelays)`), { route: "relais", benin: true });
|
|
110
|
+
}
|
|
111
|
+
} catch { /* jamais bloquant */ }
|
|
112
|
+
res.setHeader("Retry-After", "2");
|
|
113
|
+
refuserEnTexte(res, 503, "Trop de transferts en cours, réessayez dans un instant");
|
|
114
|
+
return;
|
|
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 };
|
|
119
|
+
relaisEnCours += 1;
|
|
120
|
+
try { await travail(bornes); } finally { relaisEnCours -= 1; }
|
|
121
|
+
}
|
|
122
|
+
|
|
58
123
|
// GREFFONS de ce studio — jamais du player. `null` quand le module est absent ou coupé
|
|
59
124
|
// (`PLAYER_PLUGINS_OFF`) : chaque usage doit donc être gardé, et le player continue sans eux.
|
|
60
125
|
let docbot = null;
|
|
@@ -219,7 +284,7 @@ const { refuserEnTexte, repondreJson, repondreJsonTexte } = require("./reponses.
|
|
|
219
284
|
// exemplaires d'un fait divergent, c'est la formule du dépôt.
|
|
220
285
|
const POLITIQUE_PERMISSIONS = "camera=(), microphone=(), geolocation=(), payment=()";
|
|
221
286
|
|
|
222
|
-
async function relayerFichier(res, r, disposition) {
|
|
287
|
+
async function relayerFichier(res, r, disposition, bornes = { stallMs: relaisStallMs, maxMs: relaisMaxMs }) {
|
|
223
288
|
if (!r) { refuserEnTexte(res, 404, "Fichier indisponible"); return; }
|
|
224
289
|
// 413 et 416 sont des REFUS ARGUMENTÉS de l'amont local (plafond, borne absurde) : les fondre
|
|
225
290
|
// dans un 502 dirait « panne » là où l'amont a dit « demande irrecevable ».
|
|
@@ -299,21 +364,33 @@ async function relayerFichier(res, r, disposition) {
|
|
|
299
364
|
if (!compresse && brute) res.setHeader("Content-Length", brute);
|
|
300
365
|
|
|
301
366
|
const plafond = PLAFOND_RELAIS;
|
|
367
|
+
// Deux minuteries et un signal : « sans progression » se réarme à chaque morceau qui PASSE (si le
|
|
368
|
+
// client ne lit plus, la contre-pression arrête les morceaux et la minuterie tombe) ; « budget total »
|
|
369
|
+
// ne se réarme jamais. L'abandon passe par le signal du pipeline, qui détruit la source ET la réponse.
|
|
370
|
+
const abandon = new globalThis.AbortController();
|
|
371
|
+
let stall = null;
|
|
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);
|
|
374
|
+
rearmer();
|
|
302
375
|
try {
|
|
303
376
|
await pipeline(Readable.fromWeb(r.body), async function* (source) {
|
|
304
377
|
let vus = 0;
|
|
305
378
|
for await (const morceau of source) {
|
|
306
379
|
vus += morceau.length;
|
|
307
380
|
if (vus > plafond) throw new Error(`relais interrompu : ${vus} octets reçus, plafond ${plafond}`);
|
|
381
|
+
rearmer();
|
|
308
382
|
yield morceau;
|
|
309
383
|
}
|
|
310
|
-
}, res);
|
|
384
|
+
}, res, { signal: abandon.signal });
|
|
311
385
|
} catch (erreur) {
|
|
312
386
|
// ⚠️ ROMPRE, PAS RÉPONDRE — et le DIRE. Aucun code de retour n'est plus disponible ; ne
|
|
313
387
|
// reste que la coupure. Une coupure fréquente ici est un plafond mal réglé ou un amont
|
|
314
388
|
// défaillant : l'avaler ferait passer un défaut d'exploitation pour un caprice du réseau.
|
|
315
|
-
|
|
389
|
+
const cause = abandon.signal.aborted && abandon.signal.reason instanceof Error ? abandon.signal.reason : erreur;
|
|
390
|
+
try { PLAYER.errors.capture(cause instanceof Error ? cause : new Error(String(cause)), { route: "relais" }); } catch { /* jamais bloquant */ }
|
|
316
391
|
try { res.destroy(); } catch { /* le socket est peut-être déjà parti */ }
|
|
392
|
+
} finally {
|
|
393
|
+
clearTimeout(stall); clearTimeout(budget);
|
|
317
394
|
}
|
|
318
395
|
}
|
|
319
396
|
|
|
@@ -762,6 +839,15 @@ async function handlerMesure(req, res) {
|
|
|
762
839
|
derniereIlYaS: dernier == null ? null : Math.max(0, Math.round((Date.now() - dernier) / 1000)),
|
|
763
840
|
};
|
|
764
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
|
+
},
|
|
765
851
|
// ⚠️ CE QUE CETTE INSTANCE A VÉCU — parce que `lectureSaturee` ne répondait qu'à UNE
|
|
766
852
|
// question. « La route est-elle lente ? », « lesquelles ? », « la base ou nous ? »,
|
|
767
853
|
// « combien de 5xx ? », « la boucle décroche-t-elle ? » n'avaient aucune réponse
|
|
@@ -850,10 +936,16 @@ async function handlerMesure(req, res) {
|
|
|
850
936
|
// Placée AVANT `getPresentation` — écrite après, elle aurait laissé passer très exactement la
|
|
851
937
|
// requête qu'elle est censée épargner. Le quota se déduit de la cadence de l'audience
|
|
852
938
|
// (`src/cadence.ts`), il n'est pas choisi à la main.
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
939
|
+
// ⚠️ DEUX POINTS, DEUX CLÉS — LE QUOTA EST DÉRIVÉ « SUR CHACUN DES DEUX POINTS » ET UNE SEULE
|
|
940
|
+
// CLÉ LES FAISAIT PAYER LE MÊME BUDGET. Le filet du navigateur relit l'état ET le chat toutes
|
|
941
|
+
// les 25 s (`gabarit-live.js`) : sous `pread:<ip>`, une sortie unique portait 306 spectateurs
|
|
942
|
+
// au repos, pas les 613 annoncés — et une saturation du chat coupait l'état, qui fait autorité
|
|
943
|
+
// sur la page affichée. Reproduit par un audit externe le 13/09 contre le vrai limiteur. Une
|
|
944
|
+
// requête qui demanderait les deux points paie les deux.
|
|
945
|
+
const points = [String(q.state || "") === "1" ? "state" : null, String(q.chat || "") === "1" ? "chat" : null].filter(Boolean);
|
|
946
|
+
const ipSondage = points.length ? (adresseAppelant(req) || "anon") : "";
|
|
947
|
+
for (const point of points) {
|
|
948
|
+
if (!(await PLAYER.limits.allow(`pread:${point}:${ipSondage}`, PRESENT_QUOTA_PER_HOUR, 3600))) {
|
|
857
949
|
// ⚠️ UN REFUS MUET FAIT UNE AUDIENCE QUI DÉCROCHE SANS CAUSE NOMMÉE. Le 429 n'apparaît
|
|
858
950
|
// que dans la console du spectateur ; l'exploitant, lui, verrait des pages qui ne tournent
|
|
859
951
|
// plus chez tout un bâtiment. Une fois par heure suffit à nommer la cause sans inonder.
|
|
@@ -976,8 +1068,10 @@ async function handlerMesure(req, res) {
|
|
|
976
1068
|
if (String(q.file || "") === "1") {
|
|
977
1069
|
if (!isAllowedStorageUrl(pres.file_url)) { refuserEnTexte(res, 404, "Fichier indisponible"); return; }
|
|
978
1070
|
const range = req.headers["range"];
|
|
979
|
-
|
|
980
|
-
|
|
1071
|
+
await relayerSousAdmission(res, async (bornes) => {
|
|
1072
|
+
const r = await PLAYER.storage.fetchFile(pres.file_url, { range });
|
|
1073
|
+
await relayerFichier(res, r, null, bornes);
|
|
1074
|
+
});
|
|
981
1075
|
return;
|
|
982
1076
|
}
|
|
983
1077
|
const supaUrl = (PLAYER.config && PLAYER.config.supabaseUrl) || "";
|
|
@@ -999,8 +1093,10 @@ async function handlerMesure(req, res) {
|
|
|
999
1093
|
if (!isAllowedStorageUrl(url)) return sendRefusal(res, "url-not-allowed", embed);
|
|
1000
1094
|
if (String(q.stream || "") === "1") {
|
|
1001
1095
|
const range = req.headers["range"];
|
|
1002
|
-
|
|
1003
|
-
|
|
1096
|
+
await relayerSousAdmission(res, async (bornes) => {
|
|
1097
|
+
const r = await PLAYER.storage.fetchFile(url, { range });
|
|
1098
|
+
await relayerFichier(res, r, dispositionInline(q.name), bornes);
|
|
1099
|
+
});
|
|
1004
1100
|
return;
|
|
1005
1101
|
}
|
|
1006
1102
|
const supaUrl = (PLAYER.config && PLAYER.config.supabaseUrl) || "";
|
|
@@ -1051,8 +1147,10 @@ async function handlerMesure(req, res) {
|
|
|
1051
1147
|
// Stream depuis le Storage en RELAYANT les requêtes Range → pdf.js charge progressivement (les 1res
|
|
1052
1148
|
// pages s'affichent sans télécharger tout le PDF) → affichage bien plus rapide.
|
|
1053
1149
|
const range = req.headers["range"];
|
|
1054
|
-
|
|
1055
|
-
|
|
1150
|
+
await relayerSousAdmission(res, async (bornes) => {
|
|
1151
|
+
const r = await PLAYER.storage.fetchFile(share.file_url, { range });
|
|
1152
|
+
await relayerFichier(res, r, dispositionInline(share.file_name), bornes);
|
|
1153
|
+
});
|
|
1056
1154
|
return;
|
|
1057
1155
|
}
|
|
1058
1156
|
|
|
@@ -1143,6 +1241,13 @@ async function handlerMesure(req, res) {
|
|
|
1143
1241
|
// ⚠️ Exporté pour être ÉPROUVÉ, pas pour être appelé : le plafond du relais ne se vérifie qu en
|
|
1144
1242
|
// regardant si le corps a été lu, ce qu aucune route ne peut montrer de l extérieur.
|
|
1145
1243
|
// ⚠️ Exporté pour être ÉPROUVÉ : « le contexte reste vivant » ne se vérifie pas de l'extérieur.
|
|
1146
|
-
module.exports = { __contexte: () => PLAYER, handler, init, TIERS, POLITIQUE_PERMISSIONS, refuserEnTexte, repondreJson, __relayerFichier: relayerFichier, __jsonPourScript: jsonPourScript
|
|
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,
|
|
1248
|
+
// ⚠️ COUTURE DE BANC, PAS D'API : le cache de lecture est global au module, et un banc qui laisse des
|
|
1249
|
+
// lectures en vol contamine le suivant (128 promesses éternelles, 503 partout — trouvé par un audit
|
|
1250
|
+
// externe sous mélange, graine 20260913). Un banc doit pouvoir VÉRIFIER qu'il rend le cache vide.
|
|
1251
|
+
__cacheLecture: cacheLecture };
|
|
1147
1252
|
|
|
1148
1253
|
// redeploy: forcer le build production (Vercel a sauté la prod du merge #463 — wording re-partage).
|
|
@@ -138,6 +138,18 @@ function viewerHtml(share, nonce, logoUrl, pitch) {
|
|
|
138
138
|
#pages{display:flex;flex-direction:column;align-items:center;gap:16px;width:max-content;min-width:100%;margin:0 auto;padding:22px 14px}
|
|
139
139
|
.page{position:relative;background:#fff;box-shadow:0 6px 22px #0006;border-radius:3px}
|
|
140
140
|
.pspace{flex:0 0 auto;width:1px;visibility:hidden} /* porte la hauteur des pages ou vignettes NON materialisees */
|
|
141
|
+
/* Le navigateur plafonne la hauteur de défilement (Chrome : 33 554 432 px) : au-delà, des pages existent et ne se
|
|
142
|
+
joignent pas. L'avis le DIT, fixé au bas du cadre, et disparaît dès que le zoom rend tout le document atteignable. */
|
|
143
|
+
/* Mode « une seule page » — la STRUCTURE vit ici, dans la visionneuse de base : enterOnePage() est offert à
|
|
144
|
+
tout greffon (PlayerBot), pas seulement à l'assistant dont la feuille (gabarit-agent) porte les animations.
|
|
145
|
+
Sans ces règles, une page hors assistant en mode une page empilait courante et voisines, et la courante
|
|
146
|
+
se retrouvait sous le cadre (mesuré dans Chrome le 13/09). Un espaceur de pages absentes est caché aussi. */
|
|
147
|
+
body.onepage .scroll{overflow:hidden}
|
|
148
|
+
body.onepage #pages{height:100%;display:flex;align-items:center;justify-content:center;padding:0}
|
|
149
|
+
body.onepage #pages .page{display:none;margin:0}
|
|
150
|
+
body.onepage #pages .page.cur{display:block}
|
|
151
|
+
body.onepage #pages .pspace{display:none}
|
|
152
|
+
.plafond{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);max-width:min(92vw,560px);background:#111d;color:#fff;border:1px solid #fff3;border-radius:8px;padding:8px 12px;font-size:13px;line-height:1.4;text-align:center;z-index:40;box-shadow:0 6px 22px #0006}
|
|
141
153
|
.page canvas{display:block;border-radius:3px}
|
|
142
154
|
/* Couche texte pdf.js : invisible, superposée au canvas → sélection du texte possible (requiert --scale-factor). */
|
|
143
155
|
.textLayer{position:absolute;inset:0;overflow:hidden;line-height:1;opacity:1;z-index:2;forced-color-adjust:none}
|
|
@@ -335,6 +347,7 @@ ${LEGAL_CSS}
|
|
|
335
347
|
get onePage(){return onePage;},
|
|
336
348
|
get reportEnAttente(){return tRestaurer!==null;},
|
|
337
349
|
get fenetre(){return pagesFenetre;},get vignFenetre(){return vignFenetre;},
|
|
350
|
+
get atteignables(){return atteignables;},
|
|
338
351
|
get soloOffered(){return soloOffered;},set soloOffered(v){soloOffered=!!v;}};
|
|
339
352
|
var pdfDoc=null, zoom=1, rot=0, firstAspect=1.35, rendered={}, io=null, ioCur=null;
|
|
340
353
|
// ATTENTION : LE LECTEUR GARDAIT TOUTES LES PAGES RENDUES (P1 audit externe).
|
|
@@ -353,6 +366,14 @@ ${LEGAL_CSS}
|
|
|
353
366
|
// FENETRE VIRTUELLE : seuls les gabarits de pages (et de vignettes) proches du visible EXISTENT
|
|
354
367
|
// dans le DOM ; deux espaceurs portent la hauteur des absents. Voir src/viewer.ts.
|
|
355
368
|
var pagesFenetre={debut:0,fin:0}, vignFenetre={debut:0,fin:0};
|
|
369
|
+
// ⚠️ LA VIRTUALISATION BORNE LE DOM, PAS LA GEOMETRIE. Les espaceurs portent la hauteur de TOUTES les
|
|
370
|
+
// pages absentes et Chrome plafonne la hauteur de defilement a 33 554 432 px : au-dela, un scrollTop
|
|
371
|
+
// pose ne mene nulle part. A 200 % sur 1 440 px, la 8 615e page d un document de 10 000 est la
|
|
372
|
+
// derniere atteignable — reproduit par un audit externe le 13/09 dans Chrome reel. Le calcul vit
|
|
373
|
+
// dans src/viewer.ts (pagesAtteignables) ; ici on l applique, on le DIT au lecteur, et on ne le
|
|
374
|
+
// laisse pas defiler vers une page qui n arrive jamais. Le remede durable est un defilement
|
|
375
|
+
// segmente ; ceci est le plafond explicite, jamais silencieux, en attendant.
|
|
376
|
+
var atteignables=0;
|
|
356
377
|
var MARGE_VIRTUELLE=3, ESPACE_PAGES=16, HAUT_PAGES=22; // = CSS #pages (gap, padding-top)
|
|
357
378
|
var VIGN_MARGE_VIRTUELLE=4, ESPACE_VIGN=10, HAUT_VIGN=12, VIGN_BORDURE=4; // = CSS .vign-in / .vg
|
|
358
379
|
var pagesAvant=null, pagesApres=null, vignAvant=null, vignApres=null, rafPages=0, rafVign=0;
|
|
@@ -962,13 +983,39 @@ ${LEGAL_CSS}
|
|
|
962
983
|
function poserEspaceur(el,hauteur,ecart){ if(hauteur>0){ el.style.display=''; el.style.height=Math.max(0,hauteur-ecart)+'px'; } else { el.style.display='none'; el.style.height='0px'; } }
|
|
963
984
|
function hauteurPage(){ return Math.round(Math.round(targetWidth())*aspectEffectif()); }
|
|
964
985
|
function geoPages(){ return {hauteurElement:hauteurPage(),ecart:ESPACE_PAGES,decalageHaut:HAUT_PAGES}; }
|
|
986
|
+
// La derniere page que le navigateur peut presenter a cette geometrie, et l avis qui va avec. En mode
|
|
987
|
+
// une page, le defilement ne sert pas a naviguer : tout est atteignable.
|
|
988
|
+
function majAtteignables(){
|
|
989
|
+
if(!numPages){ atteignables=0; return; }
|
|
990
|
+
var g=geoPages();
|
|
991
|
+
atteignables=onePage?numPages:Player.viewer.pagesAtteignables({hauteurElement:g.hauteurElement,ecart:g.ecart,decalageHaut:g.decalageHaut,total:numPages});
|
|
992
|
+
var el=document.getElementById('plafondAvis');
|
|
993
|
+
if(!el){ el=document.createElement('div'); el.id='plafondAvis'; el.className='plafond'; el.setAttribute('role','status'); el.setAttribute('aria-live','polite'); el.style.display='none'; (document.querySelector('.lmain')||document.body).appendChild(el); }
|
|
994
|
+
if(atteignables<numPages){
|
|
995
|
+
var auMin=atteignablesAuZoom(Player.viewer.MIN_ZOOM);
|
|
996
|
+
el.textContent='Ce navigateur ne peut pas faire défiler au-delà de la page '+atteignables+' sur '+numPages+' à ce zoom. '
|
|
997
|
+
+(auMin>=numPages?'Réduisez le zoom, ou passez en mode une page pour lire la suite.':'Même au zoom minimal, seules '+auMin+' pages seraient atteignables : passez en mode une page pour lire la suite.');
|
|
998
|
+
el.style.display='';
|
|
999
|
+
}
|
|
1000
|
+
else { el.style.display='none'; el.textContent=''; }
|
|
1001
|
+
}
|
|
1002
|
+
function totalDefilable(){ return onePage?numPages:(atteignables||numPages); }
|
|
1003
|
+
// Ce que le defilement continu atteindrait a un autre zoom — pour dire au lecteur si reduire suffit.
|
|
1004
|
+
function atteignablesAuZoom(z){
|
|
1005
|
+
var w=Math.round(Player.viewer.fitWidth({containerWidth:baseWidth(),containerHeight:scrollEl.clientHeight,zoom:z,onePage:false,aspect:firstAspect,rotation:rot,overlap:botOverlap(),reserve:onePageReserve()}));
|
|
1006
|
+
return Player.viewer.pagesAtteignables({hauteurElement:Math.round(w*aspectEffectif()),ecart:ESPACE_PAGES,decalageHaut:HAUT_PAGES,total:numPages});
|
|
1007
|
+
}
|
|
965
1008
|
function creerPage(i,w,h){ var d=document.createElement('div'); d.className='page ph'; d.dataset.p=i; d.style.width=w+'px'; d.style.height=h+'px'; d.textContent='Page '+i; if(io)io.observe(d); if(ioCur)ioCur.observe(d); return d; }
|
|
966
1009
|
function retirerPage(el){ var n=+el.dataset.p; libererPage(n); try{ if(io)io.unobserve(el); if(ioCur)ioCur.unobserve(el); }catch(e){} if(el.parentNode) el.parentNode.removeChild(el); }
|
|
967
1010
|
function reconcilierPages(force,autour){
|
|
968
1011
|
if(!pagesEl||!pagesAvant||!pagesApres||!numPages)return;
|
|
969
1012
|
var g=geoPages(), f;
|
|
970
|
-
|
|
971
|
-
|
|
1013
|
+
// ⚠️ EN MODE UNE PAGE, AUCUN ESPACEUR : la page courante est centree par flex et les autres sont
|
|
1014
|
+
// display:none — un espaceur portant la hauteur des milliers de pages precedentes poussait la
|
|
1015
|
+
// 10 000e a 8 339 242 px du haut, presente dans le DOM, courante, et INVISIBLE. Mesure par un
|
|
1016
|
+
// audit externe dans Chrome le 13/09 : les bancs prouvaient l existence et le numero, pas l ecran.
|
|
1017
|
+
if(onePage){ var c=autour||cur||1; var d0=Math.max(1,c-1), f0=Math.min(numPages,c+1); f={debut:d0,fin:f0,avant:0,apres:0}; }
|
|
1018
|
+
else f=Player.viewer.fenetreVirtuelle({debutVisible:scrollEl.scrollTop||0,hauteurVisible:scrollEl.clientHeight||0,hauteurElement:g.hauteurElement,ecart:g.ecart,decalageHaut:g.decalageHaut,total:totalDefilable(),marge:MARGE_VIRTUELLE});
|
|
972
1019
|
if(!force&&f.debut===pagesFenetre.debut&&f.fin===pagesFenetre.fin)return;
|
|
973
1020
|
pagesFenetre=f;
|
|
974
1021
|
var w=Math.round(targetWidth()), h=g.hauteurElement, presents={};
|
|
@@ -1000,6 +1047,7 @@ ${LEGAL_CSS}
|
|
|
1000
1047
|
pagesEl.innerHTML='';
|
|
1001
1048
|
pagesAvant=espaceur('pagesAvant'); pagesApres=espaceur('pagesApres'); pagesEl.appendChild(pagesAvant); pagesEl.appendChild(pagesApres);
|
|
1002
1049
|
pagesFenetre={debut:0,fin:0};
|
|
1050
|
+
majAtteignables();
|
|
1003
1051
|
reconcilierPages(true,cur||1);
|
|
1004
1052
|
var _band=capReserve(); var _pb=document.body.classList.contains('botplayer')?(document.body.classList.contains('vsplit')?Math.round(window.innerHeight*0.38)+50:(isLand()?0:240)):(botOverlap()+(_band?_band+12:(document.body.classList.contains('deskaudio')?16:0))); pagesEl.style.paddingBottom = onePage ? (_pb+'px') : ''; // centre la page dans l'espace VISIBLE (au-dessus de la sheet mobile / du bandeau desktop / sous le header en audio seul)
|
|
1005
1053
|
if(onePage) showPage(cur||1);
|
|
@@ -1032,6 +1080,9 @@ ${LEGAL_CSS}
|
|
|
1032
1080
|
function scrollToPage(p){
|
|
1033
1081
|
// Toute navigation explicite PERIME le report en attente : c est ce qui ferme la course.
|
|
1034
1082
|
clearTimeout(tRestaurer); tRestaurer=null;
|
|
1083
|
+
// Une page au-dela du plafond de defilement n arrive jamais : on s arrete a la derniere atteignable,
|
|
1084
|
+
// et l avis (deja visible dans ce cas) dit pourquoi.
|
|
1085
|
+
if(!onePage&&atteignables&&p>atteignables) p=atteignables;
|
|
1035
1086
|
var el=pagesEl.querySelector('.page[data-p="'+p+'"]');
|
|
1036
1087
|
// La page peut ne pas EXISTER encore : on se place a sa position calculee, on materialise, puis on aligne.
|
|
1037
1088
|
if(!el){ scrollEl.scrollTop=Player.viewer.positionDe(p,geoPages()); reconcilierPages(true,p); el=pagesEl.querySelector('.page[data-p="'+p+'"]'); }
|
|
@@ -1169,7 +1220,7 @@ ${LEGAL_CSS}
|
|
|
1169
1220
|
function reconcilierVignettes(force){
|
|
1170
1221
|
if(!vignIn||!vignAvant||!vignApres||!vignOuvert||numPages<2)return;
|
|
1171
1222
|
var g=geoVign();
|
|
1172
|
-
var f=Player.viewer.fenetreVirtuelle({debutVisible:vignIn.scrollTop||0,hauteurVisible:vignIn.clientHeight||0,hauteurElement:g.hauteurElement,ecart:g.ecart,decalageHaut:g.decalageHaut,total:numPages,marge:VIGN_MARGE_VIRTUELLE});
|
|
1223
|
+
var f=Player.viewer.fenetreVirtuelle({debutVisible:vignIn.scrollTop||0,hauteurVisible:vignIn.clientHeight||0,hauteurElement:g.hauteurElement,ecart:g.ecart,decalageHaut:g.decalageHaut,total:Math.min(numPages,Player.viewer.pagesAtteignables({hauteurElement:g.hauteurElement,ecart:g.ecart,decalageHaut:g.decalageHaut,total:numPages})),marge:VIGN_MARGE_VIRTUELLE});
|
|
1173
1224
|
if(!force&&f.debut===vignFenetre.debut&&f.fin===vignFenetre.fin)return;
|
|
1174
1225
|
vignFenetre=f;
|
|
1175
1226
|
var presents={}, els=vignIn.querySelectorAll('.vg');
|
package/server/retention.js
CHANGED
|
@@ -17,7 +17,15 @@
|
|
|
17
17
|
// confronte les colonnes du schéma vivant à docs/RETENTION.md.
|
|
18
18
|
|
|
19
19
|
let PLAYER = null;
|
|
20
|
-
|
|
20
|
+
// ⚠️ TROISIÈME ÉTAT : `storage.remove` ABSENT. Un hôte qui fournit `put` sans `remove` fabrique des
|
|
21
|
+
// objets, et le balayage effaçait leur ligne « comme avant » — le seul chemin vers l'objet, puisque
|
|
22
|
+
// la capacité expose `put` et `remove` mais jamais `list`. Trouvé par un hôte (STUDIO, 13/09) en
|
|
23
|
+
// lisant les lignes 141 et 248 de ce fichier, pas la prose du contrat, qui supposait qu'on fournit
|
|
24
|
+
// un `remove`. Le correctif de 0.1.164 distinguait « a échoué » de « a réussi » ; il ne voyait pas
|
|
25
|
+
// « n'a pas été tenté ». Désormais : ligne RETENUE, comptée, et la capacité manquante est dite une
|
|
26
|
+
// fois par processus — ici, pas à chaque ligne, sinon un balayage de mille lignes crie mille fois.
|
|
27
|
+
let sansRemove = false, sansRemoveDit = false;
|
|
28
|
+
const init = (ctx) => { PLAYER = ctx; sansRemove = false; sansRemoveDit = false; };
|
|
21
29
|
const enc = encodeURIComponent;
|
|
22
30
|
|
|
23
31
|
// Fenêtres par défaut de docs/RETENTION.md — l'hôte ajuste via `config.retention`.
|
|
@@ -137,9 +145,23 @@ async function effacerParIds(table, filtre, colId, ids, opts) {
|
|
|
137
145
|
// retrait donnerait deux chemins de destruction, dont un seul serait gardé. C'est exactement ce que
|
|
138
146
|
// `retentionUnePorte.test.js` refuse de laisser arriver.
|
|
139
147
|
async function retirerFichier(bucket, chemin, opts) {
|
|
140
|
-
if (
|
|
148
|
+
if (!chemin) return null; // null = rien à retirer (ou dry-run) ; true = retiré ; false = pas retiré
|
|
141
149
|
const retirer = PLAYER.storage && typeof PLAYER.storage.remove === "function" ? PLAYER.storage.remove.bind(PLAYER.storage) : null;
|
|
142
|
-
if (!retirer)
|
|
150
|
+
if (!retirer) {
|
|
151
|
+
// Pas de capacité : l'objet ne peut PAS être retiré, donc la ligne ne doit pas partir. Même en
|
|
152
|
+
// dry-run on le note, pour qu'un hôte le lise AVANT d'armer le balayage.
|
|
153
|
+
sansRemove = true;
|
|
154
|
+
if (!sansRemoveDit) {
|
|
155
|
+
sansRemoveDit = true;
|
|
156
|
+
try {
|
|
157
|
+
PLAYER.errors.capture(new Error("rétention : `storage.remove` n'est pas fourni — les lignes porteuses de fichiers"
|
|
158
|
+
+ " sont RETENUES (leur objet resterait sinon inatteignable, la capacité n'exposant jamais `list`)."
|
|
159
|
+
+ " Fournissez `storage.remove` pour qu'elles partent."), { route: "retention", benin: true });
|
|
160
|
+
} catch { /* jamais bloquant */ }
|
|
161
|
+
}
|
|
162
|
+
return opts.dryRun ? null : false;
|
|
163
|
+
}
|
|
164
|
+
if (opts.dryRun) return null;
|
|
143
165
|
try { return !!(await retirer(bucket, chemin)); } catch { return false; }
|
|
144
166
|
}
|
|
145
167
|
|
|
@@ -333,6 +355,7 @@ async function purgerCacheDeVoix(opts, borneDate) {
|
|
|
333
355
|
}
|
|
334
356
|
|
|
335
357
|
async function purgerRetention(now, optsBrutes = {}) {
|
|
358
|
+
sansRemove = false; // le rapport dit ce qui a manqué pendant CE passage, pas pendant la vie du processus
|
|
336
359
|
let f, opts;
|
|
337
360
|
try { f = fenetresValidees(); opts = optionsValidees(optsBrutes); }
|
|
338
361
|
catch (e) {
|
|
@@ -436,7 +459,8 @@ async function purgerRetention(now, optsBrutes = {}) {
|
|
|
436
459
|
doc_presentation_attendees: presRapport.presences,
|
|
437
460
|
pieces_jointes: presRapport.fichiers,
|
|
438
461
|
};
|
|
439
|
-
|
|
462
|
+
// `sansRemove` : la capacité manquait pendant CE passage, et `retenues` en porte la trace.
|
|
463
|
+
return { ok: true, dryRun: !!opts.dryRun, sansRemove, efface, rapport };
|
|
440
464
|
}
|
|
441
465
|
|
|
442
466
|
// Balayage opportuniste : au plus UN par fenêtre de 24 h (le verrou est le compteur de débit
|
package/server/routes-liens.js
CHANGED
|
@@ -12,6 +12,18 @@ const { SESSION_QUOTA_PER_HOUR, VIEW_QUOTA_PER_HOUR } = require("./shared.genera
|
|
|
12
12
|
let PLAYER = null;
|
|
13
13
|
const init = (ctx) => { PLAYER = ctx; };
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Le motif qu'un crochet de courrier DÉCLARE avec son refus (`reason` ou `motif`), borné : une chaîne
|
|
17
|
+
* courte, ou rien. Jamais un objet, jamais plus de 80 caractères — c'est un mot de l'hôte à son
|
|
18
|
+
* appelant, pas un canal.
|
|
19
|
+
*/
|
|
20
|
+
function motifDeclare(r) {
|
|
21
|
+
const m = r && (r.reason !== undefined ? r.reason : r.motif);
|
|
22
|
+
if (m === null || m === undefined || typeof m === "object") return null;
|
|
23
|
+
const t = String(m).trim().slice(0, 80);
|
|
24
|
+
return t || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
// Traite les actions de cette famille. Le MARQUEUR est le retour : les blocs répondent puis
|
|
16
28
|
// sortent par leurs `return` d'origine (valeur ≠ false) ; si aucune action ne correspond, la
|
|
17
29
|
// chute au bout rend `false` et le dispatch continue. Aucune liste d'actions n'est dupliquée
|
|
@@ -301,7 +313,7 @@ async function traiter(req, res, body, slug) {
|
|
|
301
313
|
try { out = await createReshare(body.slug || slug, { email: mail, name: body.name, clientKey: body.clientKey }); } catch { /* parent introuvable */ }
|
|
302
314
|
if (!out) return j(404, { ok: false });
|
|
303
315
|
let sent = false;
|
|
304
|
-
let refusEnvoi = null;
|
|
316
|
+
let refusEnvoi = null, motifHote = null;
|
|
305
317
|
// ⚠️ UN LIEN IDEMPOTENT NE RENVOIE PAS DE COURRIER, ET C'EST L'AUTRE MOITIÉ DU CORRECTIF.
|
|
306
318
|
// Rendre le même enfant tout en réexpédiant laisserait le défaut entier : le destinataire
|
|
307
319
|
// reçoit deux messages, ce qui est exactement ce qu'on répare. Trouvé par le banc de
|
|
@@ -364,7 +376,15 @@ async function traiter(req, res, body, slug) {
|
|
|
364
376
|
const origin = publique;
|
|
365
377
|
const r = await sendReshareEmail({ parent, childSlug: out.slug, origin, toEmail: mail, toName: body.name });
|
|
366
378
|
sent = !!(r && r.sent);
|
|
367
|
-
if (!sent)
|
|
379
|
+
if (!sent) {
|
|
380
|
+
refusEnvoi = refusEnvoi || "host-declined";
|
|
381
|
+
// ⚠️ L'HÔTE DIT POURQUOI, ET ON LE JETAIT. Un hôte (ADV, 13/09) répond délibérément
|
|
382
|
+
// `{ sent: false, motif }` à chaque refus — huit motifs distincts — précisément pour que
|
|
383
|
+
// « refusé » ne se confonde pas avec « en panne ». On ne lisait que `sent` : la
|
|
384
|
+
// désambiguïsation que le contrat disait manquante, au moins un hôte l'envoyait déjà.
|
|
385
|
+
// Bornée et recopiée telle quelle : c'est un mot de l'hôte à son propre appelant.
|
|
386
|
+
motifHote = motifDeclare(r);
|
|
387
|
+
}
|
|
368
388
|
} catch { /* best-effort : le lien existe quand même */ }
|
|
369
389
|
}
|
|
370
390
|
// ⚠️ `sent: false` MENTAIT QUAND LA VÉRITÉ ÉTAIT « JE NE SAIS PAS », ET C'EST CE MENSONGE QUI
|
|
@@ -393,7 +413,7 @@ async function traiter(req, res, body, slug) {
|
|
|
393
413
|
// Le refus se DIT : « rien n'est parti » et « l'envoi n'était pas permis » ne se
|
|
394
414
|
// ressemblent pas, et une interface qui les confond propose un bouton qui ne marchera
|
|
395
415
|
// jamais.
|
|
396
|
-
return j(200, { ok: true, slug: out.slug, sent, delivery, ...(refusEnvoi ? { sendRefused: refusEnvoi } : {}) });
|
|
416
|
+
return j(200, { ok: true, slug: out.slug, sent, delivery, ...(refusEnvoi ? { sendRefused: refusEnvoi } : {}), ...(motifHote ? { hostReason: motifHote } : {}) });
|
|
397
417
|
}
|
|
398
418
|
// ⚠️ CE REPLI NE COUVRE QUE LES ÉVÉNEMENTS ANALYTIQUES (P2 huitième audit). Une action POST
|
|
399
419
|
// qu'aucune famille n'a reconnue tombait ici et repartait `{"ok":true}` — une faute de
|
|
@@ -8,7 +8,56 @@ const { repondreJson } = require("./reponses.js");
|
|
|
8
8
|
|
|
9
9
|
const { getShareBySlug } = require("./shares");
|
|
10
10
|
let PLAYER = null;
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
// ⚠️ LA VÉRIFICATION N'AVAIT AUCUN PLAFOND — seule la DEMANDE de code en avait un (20/h par adresse).
|
|
13
|
+
// `visitor-verify` et `visitor-google` appelaient le greffon directement : mille tentatives depuis
|
|
14
|
+
// une adresse, zéro appel au limiteur (reproduit par un audit externe le 13/09). Un code court sans
|
|
15
|
+
// compteur dans le greffon se force ; une vérification Google par requête anonyme est une
|
|
16
|
+
// amplification réseau. Le player ne peut pas supposer que le greffon compte — c'est la même règle
|
|
17
|
+
// que la liaison session ↔ document de l'assistant : une propriété de sécurité ne dépend pas d'un
|
|
18
|
+
// code que le player ne contient pas.
|
|
19
|
+
//
|
|
20
|
+
// Deux dimensions pour le code, parce qu'une seule se contourne : par ADRESSE (une adresse ne
|
|
21
|
+
// recommence pas à zéro en changeant d'email) et par IDENTITÉ (plusieurs adresses ne forcent pas un
|
|
22
|
+
// même email). Les compteurs sont pris À L'ADMISSION, donc réussite, échec et exception les
|
|
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.
|
|
28
|
+
const VERIF_PAR_ADRESSE = 100, VERIF_PAR_IDENTITE = 10, VERIF_FENETRE_IDENTITE_S = 900;
|
|
29
|
+
const GOOGLE_PAR_ADRESSE = 100, DEMANDE_PAR_IDENTITE = 5;
|
|
30
|
+
// ⚠️ UN SHA-256 D'EMAIL N'EST PAS UNE ANONYMISATION : il se renverse par dictionnaire — qui lit la
|
|
31
|
+
// table des compteurs, une sauvegarde ou un outil d'administration précalcule les empreintes des
|
|
32
|
+
// adresses probables (relevé par un audit externe le 13/09 ; `alice@example.com` → une valeur connue).
|
|
33
|
+
// La clé vient donc du GREFFON quand il sait la produire : `visitors.rateLimitKey(email)` rend un HMAC
|
|
34
|
+
// stable et opaque, avec un secret que le player ne voit jamais et une séparation de domaine
|
|
35
|
+
// (`"visitor-email\0" + email normalisé`). Sans cette capacité, ou si elle échoue, on retombe sur
|
|
36
|
+
// l'empreinte — pseudonyme, pas secrète — et on le DIT une fois : refuser la limite serait pire que la
|
|
37
|
+
// limiter sous un pseudonyme faible, et la taire serait pire que les deux.
|
|
38
|
+
const normaliserEmail = (email) => String(email || "").trim().toLowerCase();
|
|
39
|
+
const empreinteIdentite = (email) =>
|
|
40
|
+
require("crypto").createHash("sha256").update(normaliserEmail(email)).digest("hex").slice(0, 32);
|
|
41
|
+
let repliDit = false;
|
|
42
|
+
async function cleIdentite(V, email) {
|
|
43
|
+
const norm = normaliserEmail(email);
|
|
44
|
+
if (V && typeof V.rateLimitKey === "function") {
|
|
45
|
+
try {
|
|
46
|
+
const k = await V.rateLimitKey(norm);
|
|
47
|
+
if (typeof k === "string" && k.trim()) return "h:" + k.trim().slice(0, 64);
|
|
48
|
+
throw new Error("rateLimitKey a rendu autre chose qu'une chaîne non vide");
|
|
49
|
+
} catch (e) { direLeRepli(`visitors.rateLimitKey a échoué (${e && e.message ? e.message : "cause inconnue"})`); }
|
|
50
|
+
} else direLeRepli("visitors.rateLimitKey n'est pas fourni");
|
|
51
|
+
return "e:" + empreinteIdentite(norm);
|
|
52
|
+
}
|
|
53
|
+
function direLeRepli(pourquoi) {
|
|
54
|
+
if (repliDit) return;
|
|
55
|
+
repliDit = true;
|
|
56
|
+
try {
|
|
57
|
+
PLAYER.errors.capture(new Error(`mur visiteur : ${pourquoi} — les compteurs par identité utilisent une EMPREINTE de l'email (SHA-256 tronqué), pseudonyme mais renversable par dictionnaire. Fournissez rateLimitKey (HMAC, secret côté hôte, séparation de domaine).`), { route: "visitor", benin: true });
|
|
58
|
+
} catch { /* jamais bloquant */ }
|
|
59
|
+
}
|
|
60
|
+
const init = (ctx) => { PLAYER = ctx; repliDit = false; };
|
|
12
61
|
|
|
13
62
|
// Traite les actions de cette famille. Le MARQUEUR est le retour : les blocs répondent puis
|
|
14
63
|
// sortent par leurs `return` d'origine (valeur ≠ false) ; si aucune action ne correspond, la
|
|
@@ -25,6 +74,8 @@ async function traiter(req, res, body, _slug) {
|
|
|
25
74
|
const ip = adresseAppelant(req) || "ip";
|
|
26
75
|
if (body.action === "visitor-request") {
|
|
27
76
|
if (!(await PLAYER.limits.allow(`vcode:${ip}`, 20, 3600))) return jv(429, { ok: false, error: "rate" });
|
|
77
|
+
// Une boîte ne se fait pas inonder depuis cent adresses : cinq codes par heure et par email.
|
|
78
|
+
if (!(await PLAYER.limits.allow(`vcode:id:${await cleIdentite(V, body.email)}`, DEMANDE_PAR_IDENTITE, 3600))) return jv(429, { ok: false, error: "rate" });
|
|
28
79
|
const sh = await getShareBySlug(String(body.slug || ""));
|
|
29
80
|
return jv(200, await V.requestCode(body.email, { title: sh && sh.doc_title }));
|
|
30
81
|
}
|
|
@@ -50,10 +101,13 @@ async function traiter(req, res, body, _slug) {
|
|
|
50
101
|
}
|
|
51
102
|
};
|
|
52
103
|
if (body.action === "visitor-google") {
|
|
104
|
+
if (!(await PLAYER.limits.allow(`vgoogle:${ip}`, GOOGLE_PAR_ADRESSE, 3600))) return jv(429, { ok: false, error: "rate" });
|
|
53
105
|
const r = await V.verifyGoogle(body.credential);
|
|
54
106
|
if (r.ok) await recordUnlock(r.visitor, "google");
|
|
55
107
|
return r.ok ? jv(200, { ok: true }, r.setCookie) : jv(400, r);
|
|
56
108
|
}
|
|
109
|
+
if (!(await PLAYER.limits.allow(`vverif:${ip}`, VERIF_PAR_ADRESSE, 3600))) return jv(429, { ok: false, error: "rate" });
|
|
110
|
+
if (!(await PLAYER.limits.allow(`vverif:id:${await cleIdentite(V, body.email)}`, VERIF_PAR_IDENTITE, VERIF_FENETRE_IDENTITE_S))) return jv(429, { ok: false, error: "rate" });
|
|
57
111
|
const r = await V.verifyCode(body.email, body.code, body.name);
|
|
58
112
|
if (r.ok) await recordUnlock(r.visitor, "email");
|
|
59
113
|
return r.ok ? jv(200, { ok: true }, r.setCookie) : jv(400, r);
|
package/types/context.d.ts
CHANGED
|
@@ -128,6 +128,18 @@ 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 ; 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. */
|
|
135
|
+
maxConcurrentRelays?: number;
|
|
136
|
+
/** Un relais sans progression pendant ce délai est abandonné — source et réponse détruites, place
|
|
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. */
|
|
140
|
+
relayStallMs?: number;
|
|
141
|
+
/** Durée totale maximale d'un relais (défaut 900 000 ms ; même plage que `relayStallMs`). */
|
|
142
|
+
relayMaxMs?: number;
|
|
131
143
|
/** Ce qui est POSÉ, à côté de ce que le code SAIT faire : la carte d'identité publie les deux,
|
|
132
144
|
* parce qu'une capacité disponible mais non configurée se comporte comme une absence. */
|
|
133
145
|
separateIssuer?: boolean;
|