discovery-media-player 0.1.130 → 0.1.133
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 +14 -1
- package/package.json +1 -1
- package/server/handler.js +31 -9
- package/server/retention.js +33 -1
- package/server/routes-agent.js +57 -5
package/bin/serve.js
CHANGED
|
@@ -119,7 +119,20 @@ const serveur = http.createServer(async (req, res) => {
|
|
|
119
119
|
|
|
120
120
|
// Mode dossier : la racine du serveur montre ce qu'il y a à lire.
|
|
121
121
|
if ((url.pathname === "/" || url.pathname === "/preview" || url.pathname === "/preview/") && process.env.PLAYER_LOCAL_ROOT) {
|
|
122
|
-
res.writeHead(200, {
|
|
122
|
+
res.writeHead(200, {
|
|
123
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
124
|
+
"Cache-Control": "no-store",
|
|
125
|
+
// ⚠️ Les MÊMES protections que la visionneuse, écrites pour la page qui n'y passait pas.
|
|
126
|
+
// C'était la seule page HTML servie sans CSP ni `nosniff`, encadrable par n'importe qui —
|
|
127
|
+
// relevé par le premier scan ZAP baseline (24/08). Elle n'a ni script, ni image, ni
|
|
128
|
+
// formulaire : tout est refusé, sauf son unique bloc <style>.
|
|
129
|
+
"X-Content-Type-Options": "nosniff",
|
|
130
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
131
|
+
// La définition vit dans le player (une seule copie du fait) — voir son commentaire.
|
|
132
|
+
"Permissions-Policy": player.POLITIQUE_PERMISSIONS,
|
|
133
|
+
"Content-Security-Policy":
|
|
134
|
+
"default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
|
|
135
|
+
});
|
|
123
136
|
res.end(await pageAccueil(path.resolve(process.env.PLAYER_LOCAL_ROOT)));
|
|
124
137
|
return;
|
|
125
138
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.133",
|
|
4
4
|
"description": "Self-hosted document viewer: per-recipient tracked links, reading analytics, live presentation. The core knows nothing about the application hosting it — everything it borrows arrives through an injected context.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pdf-viewer",
|
package/server/handler.js
CHANGED
|
@@ -186,18 +186,38 @@ const { adresseAppelant } = require("./appelant");
|
|
|
186
186
|
*/
|
|
187
187
|
const PLAFOND_RELAIS = Number(process.env.PLAYER_MAX_RELAY_BYTES || 0) || 60 * 1024 * 1024;
|
|
188
188
|
|
|
189
|
+
// ⚠️ UN REFUS EN TEXTE POSE SON TYPE, COMME TOUT LE RESTE. Ces réponses partaient avec un code et
|
|
190
|
+
// un corps, sans Content-Type : le seul corps de ce serveur qu'un navigateur avait le droit de
|
|
191
|
+
// deviner, alors que la règle `nosniff` est écrite partout ailleurs. Relevé par le premier scan
|
|
192
|
+
// ZAP baseline (règle 10019, 24/08).
|
|
193
|
+
function refuserEnTexte(res, statut, message) {
|
|
194
|
+
res.statusCode = statut;
|
|
195
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
196
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
197
|
+
res.end(message);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ⚠️ CE QUE LES PAGES N'UTILISENT PAS EST REFUSÉ PAR ÉCRIT. Sans Permissions-Policy, un script
|
|
201
|
+
// tiers compromis (le risque exact que l'inventaire TIERS épingle) hérite de tout ce que le
|
|
202
|
+
// navigateur sait faire — caméra, micro, position. Aucune page ici ne s'en sert : on le dit.
|
|
203
|
+
// Le plein écran n'est PAS refusé — la présentation s'en sert (page-visionneuse.js). Relevé par
|
|
204
|
+
// le premier passage CI du scan ZAP (règle 10063, 24/08) ; le passage local, scanners par
|
|
205
|
+
// défaut, ne l'avait pas vu. UNE définition, exportée : bin/serve.js pose la même — deux
|
|
206
|
+
// exemplaires d'un fait divergent, c'est la formule du dépôt.
|
|
207
|
+
const POLITIQUE_PERMISSIONS = "camera=(), microphone=(), geolocation=(), payment=()";
|
|
208
|
+
|
|
189
209
|
async function relayerFichier(res, r, disposition) {
|
|
190
|
-
if (!r) { res
|
|
210
|
+
if (!r) { refuserEnTexte(res, 404, "Fichier indisponible"); return; }
|
|
191
211
|
// 413 et 416 sont des REFUS ARGUMENTÉS de l'amont local (plafond, borne absurde) : les fondre
|
|
192
212
|
// dans un 502 dirait « panne » là où l'amont a dit « demande irrecevable ».
|
|
193
213
|
if (!r.ok && r.status !== 206) {
|
|
194
|
-
res
|
|
195
|
-
|
|
214
|
+
refuserEnTexte(res, r.status === 413 || r.status === 416 ? r.status : 502,
|
|
215
|
+
r.status === 413 ? "Fichier trop volumineux" : "Fichier indisponible");
|
|
196
216
|
return;
|
|
197
217
|
}
|
|
198
218
|
|
|
199
219
|
const compresse = !!r.headers.get("content-encoding");
|
|
200
|
-
if (compresse && r.status === 206) { res
|
|
220
|
+
if (compresse && r.status === 206) { refuserEnTexte(res, 502, "Fichier indisponible"); return; }
|
|
201
221
|
|
|
202
222
|
// ⚠️ DEUX BORNES, ET LA SECONDE EST LA SEULE QUI TIENNE DEVANT UN AMONT QUI MENT.
|
|
203
223
|
//
|
|
@@ -213,8 +233,7 @@ async function relayerFichier(res, r, disposition) {
|
|
|
213
233
|
const annoncee = Number(brute || 0);
|
|
214
234
|
if (annoncee > PLAFOND_RELAIS) {
|
|
215
235
|
try { PLAYER.errors.capture(new Error(`relais refusé : ${annoncee} octets au-dessus du plafond de ${PLAFOND_RELAIS}`), { route: "relais" }); } catch { /* jamais bloquant */ }
|
|
216
|
-
res
|
|
217
|
-
res.end("Fichier trop volumineux");
|
|
236
|
+
refuserEnTexte(res, 413, "Fichier trop volumineux");
|
|
218
237
|
// ⚠️ Renoncer ne suffit pas : un corps jamais tiré laisse la connexion amont OUVERTE, et le
|
|
219
238
|
// pool de sockets s'épuise sur les gros fichiers — exactement la ressource qu'on protège.
|
|
220
239
|
try { if (r.body) r.body.cancel(); } catch { /* déjà refermé */ }
|
|
@@ -296,6 +315,7 @@ function sendHtml(res, status, html, scriptSrc, imgExtra, frameAncestors) {
|
|
|
296
315
|
res.setHeader("Cache-Control", "no-store, max-age=0");
|
|
297
316
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
298
317
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
318
|
+
res.setHeader("Permissions-Policy", POLITIQUE_PERMISSIONS);
|
|
299
319
|
res.setHeader("Content-Security-Policy", [
|
|
300
320
|
"default-src 'none'",
|
|
301
321
|
// ⚠️ `'none'` DOIT ÊTRE SEUL ou la directive est invalide — « 'none' 'self' » a fait rejeter
|
|
@@ -339,6 +359,7 @@ function sendSoftWallHtml(res, html, nonce, imgExtra, frameAncestors) {
|
|
|
339
359
|
res.setHeader("Cache-Control", "no-store, max-age=0");
|
|
340
360
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
341
361
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
362
|
+
res.setHeader("Permissions-Policy", POLITIQUE_PERMISSIONS);
|
|
342
363
|
res.setHeader("Content-Security-Policy", [
|
|
343
364
|
"default-src 'none'",
|
|
344
365
|
`script-src 'nonce-${nonce}' https://accounts.google.com/gsi/client`,
|
|
@@ -427,6 +448,7 @@ function sendPresentHtml(res, html, nonce, supaUrl, imgExtra, frameAncestors) {
|
|
|
427
448
|
res.setHeader("Cache-Control", "no-store, max-age=0");
|
|
428
449
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
429
450
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
451
|
+
res.setHeader("Permissions-Policy", POLITIQUE_PERMISSIONS);
|
|
430
452
|
res.setHeader("Content-Security-Policy", [
|
|
431
453
|
"default-src 'none'",
|
|
432
454
|
`script-src 'nonce-${nonce}' 'self' https://cdn.jsdelivr.net https://unpkg.com https://maps.googleapis.com https://maps.gstatic.com`,
|
|
@@ -533,7 +555,7 @@ async function handler(req, res) {
|
|
|
533
555
|
if (!octets) {
|
|
534
556
|
try { octets = global.__actifsPdfjs[fichier] = require("node:fs").readFileSync(require.resolve(fichier)); } catch { /* dépendance absente */ }
|
|
535
557
|
}
|
|
536
|
-
if (!octets) { res
|
|
558
|
+
if (!octets) { refuserEnTexte(res, 404, "actif indisponible"); return; }
|
|
537
559
|
res.statusCode = 200;
|
|
538
560
|
res.setHeader("Content-Type", "text/javascript; charset=utf-8");
|
|
539
561
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
@@ -845,7 +867,7 @@ async function handler(req, res) {
|
|
|
845
867
|
const pres = await getPresentation(String(q.present));
|
|
846
868
|
if (!pres) return sendRefusal(res, "ended", embed);
|
|
847
869
|
if (String(q.file || "") === "1") {
|
|
848
|
-
if (!isAllowedStorageUrl(pres.file_url)) { res
|
|
870
|
+
if (!isAllowedStorageUrl(pres.file_url)) { refuserEnTexte(res, 404, "Fichier indisponible"); return; }
|
|
849
871
|
const range = req.headers["range"];
|
|
850
872
|
const r = await PLAYER.storage.fetchFile(pres.file_url, { range });
|
|
851
873
|
await relayerFichier(res, r, null);
|
|
@@ -1013,6 +1035,6 @@ async function handler(req, res) {
|
|
|
1013
1035
|
// tenir une seconde liste — c'est la seule façon qu'une empreinte périmée finisse par se voir.
|
|
1014
1036
|
// ⚠️ Exporté pour être ÉPROUVÉ, pas pour être appelé : le plafond du relais ne se vérifie qu en
|
|
1015
1037
|
// regardant si le corps a été lu, ce qu aucune route ne peut montrer de l extérieur.
|
|
1016
|
-
module.exports = { handler, init, TIERS, __relayerFichier: relayerFichier, __jsonPourScript: jsonPourScript };
|
|
1038
|
+
module.exports = { handler, init, TIERS, POLITIQUE_PERMISSIONS, __relayerFichier: relayerFichier, __jsonPourScript: jsonPourScript };
|
|
1017
1039
|
|
|
1018
1040
|
// redeploy: forcer le build production (Vercel a sauté la prod du merge #463 — wording re-partage).
|
package/server/retention.js
CHANGED
|
@@ -124,6 +124,30 @@ async function retirerFichier(chemin, opts) {
|
|
|
124
124
|
// au RESTE du plafond (`min(taille, plafond - examinees)`) pour ne jamais le dépasser. Et on
|
|
125
125
|
// compte les lignes RENDUES par le DELETE (`return=representation&select=id`), pas les ids
|
|
126
126
|
// présélectionnés : deux exécutions concurrentes n'annoncent pas deux fois la même suppression.
|
|
127
|
+
/**
|
|
128
|
+
* « Cette table n'existe pas ici » — et RIEN d'autre.
|
|
129
|
+
*
|
|
130
|
+
* ⚠️ UNE CIBLE DE PURGE PEUT LÉGITIMEMENT MANQUER, LE CONTRAT LE DIT. `docs/HOST-CONTRACT.md` :
|
|
131
|
+
* les migrations de débit « ne sont délibérément pas » dans le périmètre de la carte, parce
|
|
132
|
+
* qu'« un hôte peut fournir sa propre capacité `limits`, et sur un tel hôte leur absence est
|
|
133
|
+
* NORMALE, pas un défaut ». Le balayage, lui, purgeait `player_rate_limits` sans condition — donc
|
|
134
|
+
* sur cet hôte-là il levait à la cinquième cible sur huit, et les liens révoqués comme les
|
|
135
|
+
* présentations n'étaient JAMAIS atteints. Enveloppé dans un `catch` qui le classe bénin, il
|
|
136
|
+
* restait armé, silencieux, et partiellement inopérant : le pire des trois états.
|
|
137
|
+
*
|
|
138
|
+
* ⚠️ AUSSI ÉTROIT QUE `signatureAbsente`, ET POUR LA MÊME RAISON. On accepte le code PostgREST de la
|
|
139
|
+
* table introuvable, ou son message — et rien d'autre. Un 404 seul ne suffit pas : il peut venir
|
|
140
|
+
* d'ailleurs, et avaler une vraie panne ferait de ce correctif une purge qui ne purge plus sans le
|
|
141
|
+
* dire, c'est-à-dire exactement ce qu'il corrige.
|
|
142
|
+
*/
|
|
143
|
+
function tableAbsente(erreur) {
|
|
144
|
+
if (!erreur) return false;
|
|
145
|
+
const code = erreur.details && (erreur.details.code || (erreur.details.error && erreur.details.error.code));
|
|
146
|
+
if (code === "PGRST205") return true;
|
|
147
|
+
const texte = String(erreur.message || "");
|
|
148
|
+
return texte.includes("PGRST205") || /Could not find the table/i.test(texte);
|
|
149
|
+
}
|
|
150
|
+
|
|
127
151
|
async function purgerParLots(table, filtre, colId, { dryRun = false, taille = LOT, plafond = PLAFOND } = {}, plafondForce) {
|
|
128
152
|
if (plafondForce !== undefined) plafond = plafondForce;
|
|
129
153
|
let examinees = 0, supprimees = 0, tronque = false, curseur = null;
|
|
@@ -200,7 +224,15 @@ async function purgerRetention(now, optsBrutes = {}) {
|
|
|
200
224
|
rapport.commercial_doc_sessions = await purgerParLots("commercial_doc_sessions", `last_at=lt.${enc(bJournaux)}`, "session_id", opts);
|
|
201
225
|
rapport.commercial_doc_internal_sessions = await purgerParLots("commercial_doc_internal_sessions", `last_at=lt.${enc(bJournaux)}`, "session_id", opts);
|
|
202
226
|
rapport.doc_bot_sessions = await purgerParLots("doc_bot_sessions", `last_at=lt.${enc(bJournaux)}`, "id", opts);
|
|
203
|
-
|
|
227
|
+
// ⚠️ LA SEULE CIBLE QUE LE CONTRAT REND OPTIONNELLE — et le balayage la traitait comme les autres.
|
|
228
|
+
// Son absence est normale sur un hôte qui fournit sa propre capacité `limits` ; elle ne doit donc
|
|
229
|
+
// pas interrompre les trois cibles qui suivent. Sautée, ET DITE : un rapport qui omettrait la
|
|
230
|
+
// ligne se lirait comme « rien à supprimer », ce qui est une autre affirmation.
|
|
231
|
+
rapport.player_rate_limits = await purgerParLots("player_rate_limits", `expires_at=lt.${enc(new Date(now).toISOString())}`, "key", opts)
|
|
232
|
+
.catch((e) => {
|
|
233
|
+
if (!tableAbsente(e)) throw e;
|
|
234
|
+
return { examinees: 0, supprimees: 0, tronque: false, sautee: "table absente — l'hôte fournit ses propres compteurs de débit" };
|
|
235
|
+
});
|
|
204
236
|
|
|
205
237
|
// Liens révoqués : seulement là où la révocation est DATÉE (0013).
|
|
206
238
|
const dateDispo = await require("./schema").attendue("revocationDatee");
|
package/server/routes-agent.js
CHANGED
|
@@ -12,6 +12,16 @@ const init = (ctx) => { PLAYER = ctx; docbot = ctx.plugins.bot; };
|
|
|
12
12
|
// chute au bout rend `false` et le dispatch continue. Aucune liste d'actions n'est dupliquée
|
|
13
13
|
// entre ici et handler (un correctif à deux exemplaires finit par diverger) — et aucun appui
|
|
14
14
|
// sur res.writableEnded, absent des `res` postiches des bancs comme de certains hôtes.
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Les actions qui AGISSENT SUR UNE SESSION EXISTANTE — c'est-à-dire toutes celles du bloc assistant
|
|
18
|
+
* sauf `bot-start`, qui la crée. La liste vit ici plutôt que dans chaque branche pour que le banc
|
|
19
|
+
* puisse la CONFRONTER à l'aiguillage : une action ajoutée à l'un et pas à l'autre fait rougir.
|
|
20
|
+
*/
|
|
21
|
+
const ACTIONS_LIEES_A_UNE_SESSION = new Set([
|
|
22
|
+
"bot-say", "bot-history", "bot-nudge", "bot-book", "bot-contact", "bot-rate", "bot-script",
|
|
23
|
+
]);
|
|
24
|
+
|
|
15
25
|
async function traiter(req, res, body, _slug) {
|
|
16
26
|
if (body.action === "bot-tts") {
|
|
17
27
|
const jp = (status, obj) => { res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(obj)); };
|
|
@@ -101,11 +111,38 @@ async function traiter(req, res, body, _slug) {
|
|
|
101
111
|
if (!allowed) return jp(429, { ok: false, error: "rate" });
|
|
102
112
|
const share = await getShareBySlug(String(body.slug || ""));
|
|
103
113
|
if (!share || !share.bot_enabled) return jp(404, { ok: false, error: "bot" });
|
|
114
|
+
// ⚠️ UNE SESSION EST LIÉE À SON DOCUMENT — VÉRIFIÉ ICI, POUR TOUTES LES ACTIONS À LA FOIS.
|
|
115
|
+
//
|
|
116
|
+
// Cette vérification vivait DANS chaque action, et trois seulement sur sept la faisaient :
|
|
117
|
+
// `bot-rate`, `bot-script` et `bot-history` (celle-ci ajoutée après coup, en 0.1.131).
|
|
118
|
+
// `bot-say`, `bot-nudge`, `bot-book` et `bot-contact` prenaient le même `sessionId` venu du
|
|
119
|
+
// client sans jamais le rattacher au document demandé.
|
|
120
|
+
//
|
|
121
|
+
// ⚠️ CE QUE ÇA OUVRAIT EST PIRE QUE CE QU'ON VENAIT DE FERMER. `bot-history` LISAIT la
|
|
122
|
+
// conversation d'un autre document ; `bot-say` y ÉCRIT et se fait répondre, `bot-contact`
|
|
123
|
+
// attache un nom, un e-mail et un téléphone à la session d'un autre client. Une porte en
|
|
124
|
+
// lecture et quatre en écriture, dans le même bloc.
|
|
125
|
+
//
|
|
126
|
+
// ⚠️ ET LA LEÇON N'EST PAS « IL EN MANQUAIT QUATRE ». Écrite sept fois, cette garde serait
|
|
127
|
+
// oubliée une huitième au prochain ajout — le défaut n'est pas la ligne absente, c'est que
|
|
128
|
+
// sa présence dépende de la mémoire de celui qui écrit l'action. Elle est donc hissée : une
|
|
129
|
+
// action qui porte une session la prouve, sans que son auteur ait à y penser.
|
|
130
|
+
//
|
|
131
|
+
// Le relevé qui a mené ici vient d'un exploitant : « une vérification que plusieurs actions
|
|
132
|
+
// font chacune pour son compte — il faut compter les ACTIONS, pas les implémentations ».
|
|
133
|
+
// La duplication était homogène, donc invisible à un balayage des variantes ; c'est la
|
|
134
|
+
// COUVERTURE qui divergeait, et rien ne la mesurait. `gardesAgent.test.js` la mesure
|
|
135
|
+
// désormais, en lisant la liste d'actions dans ce fichier même.
|
|
136
|
+
//
|
|
137
|
+
// ⚠️ `bot-start` EST EXCLUE, ET C'EST LA SEULE. Elle CRÉE la session : exiger qu'elle en
|
|
138
|
+
// prouve une fermerait l'assistant à tout le monde.
|
|
139
|
+
if (ACTIONS_LIEES_A_UNE_SESSION.has(body.action)) {
|
|
140
|
+
const liee = await docbot.getSession(String(body.sessionId || ""));
|
|
141
|
+
if (!liee || liee.share_slug !== share.slug) return jp(400, { ok: false, error: "session" });
|
|
142
|
+
}
|
|
104
143
|
const pages = Math.max(0, Math.min(500, Number(body.pages) || 0));
|
|
105
144
|
const mobile = body.mobile === 1 || body.mobile === true; // téléphone → messages courts + autoplay steps
|
|
106
145
|
if (body.action === "bot-rate") { // satisfaction (1-5 étoiles) posée depuis le bloc central du viewer
|
|
107
|
-
const sess = await docbot.getSession(String(body.sessionId || ""));
|
|
108
|
-
if (!sess || sess.share_slug !== share.slug) return jp(400, { ok: false, error: "session" });
|
|
109
146
|
// ⚠️ CE REFUS ÉTAIT INATTEIGNABLE, ET C'EST LA MESURE QUI EN PAYAIT LE PRIX.
|
|
110
147
|
//
|
|
111
148
|
// const note = Math.max(1, Math.min(5, Number(body.rating) || 0));
|
|
@@ -139,7 +176,24 @@ async function traiter(req, res, body, _slug) {
|
|
|
139
176
|
await PLAYER.db.request("doc_bot_sessions?id=eq." + encodeURIComponent(String(body.sessionId)), { method: "PATCH", headers: { Prefer: "return=minimal" }, body: cmt ? { rating: note, rating_comment: cmt } : { rating: note } });
|
|
140
177
|
return jp(200, { ok: true });
|
|
141
178
|
}
|
|
142
|
-
if (body.action === "bot-history")
|
|
179
|
+
if (body.action === "bot-history") {
|
|
180
|
+
// ⚠️ MÊME LIAISON QUE `bot-rate` ET `bot-script`, ET ELLE MANQUAIT ICI.
|
|
181
|
+
//
|
|
182
|
+
// Cette action rendait le transcript de N'IMPORTE QUELLE session à qui en connaissait
|
|
183
|
+
// l'identifiant — et cet identifiant voyage côté client. Il suffisait d'un slug avec
|
|
184
|
+
// assistant activé (le refus juste au-dessus) et d'un `sessionId` récupéré ailleurs
|
|
185
|
+
// pour lire la conversation tenue sur un autre document : des questions de prospect,
|
|
186
|
+
// c'est-à-dire de la donnée commerciale sur un client, pas un compteur d'usage.
|
|
187
|
+
//
|
|
188
|
+
// ⚠️ ON NE PEUT PAS DÉLÉGUER CE FILTRAGE AU GREFFON. `docbot` est `ctx.plugins.bot`,
|
|
189
|
+
// fourni par l'hôte : supposer qu'il recoupe la session et le document reviendrait à
|
|
190
|
+
// faire dépendre une propriété de sécurité du player d'un code qu'il ne contient pas.
|
|
191
|
+
// C'est exactement pourquoi les deux actions voisines le vérifient elles-mêmes.
|
|
192
|
+
//
|
|
193
|
+
// Les deux voisines avaient la garde ET leur banc ; celle-ci n'avait ni l'une ni
|
|
194
|
+
// l'autre, et la relecture qui a ajouté les bancs a couvert deux actions sur trois.
|
|
195
|
+
return jp(200, { ok: true, messages: await docbot.listMessages(String(body.sessionId || "")) });
|
|
196
|
+
}
|
|
143
197
|
// ⚠️ Même piège : un objet littéral répond à `constructor`. Sans `Object.hasOwn`, une
|
|
144
198
|
// langue « constructor » passait la garde et finissait interpolée dans le prompt du
|
|
145
199
|
// modèle sous la forme « function Object() { [native code] } ».
|
|
@@ -149,8 +203,6 @@ async function traiter(req, res, body, _slug) {
|
|
|
149
203
|
// Bascule de langue EN COURS de présentation : renvoie le script (traduit ou FR) — le client
|
|
150
204
|
// remplace sa liste d'étapes et rejoue le message courant dans la nouvelle langue.
|
|
151
205
|
if (body.action === "bot-script") {
|
|
152
|
-
const sess = await docbot.getSession(String(body.sessionId || ""));
|
|
153
|
-
if (!sess || sess.share_slug !== share.slug) return jp(400, { ok: false, error: "session" });
|
|
154
206
|
const sp = docbot.applyPron(await docbot.scriptedPayload(share.doc_id, blang, String(body.sessionId)), await docbot.getProfile(share.bot_profile_id));
|
|
155
207
|
if (!sp) return jp(400, { ok: false, error: "script" });
|
|
156
208
|
return jp(200, { ok: true, steps: sp.steps, voiceScript: sp.voice, message: sp.hook, closing: sp.closing, messageSay: sp.hookSay, closingSay: sp.closingSay });
|