discovery-media-player 0.1.55 → 0.1.56
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/package.json +1 -1
- package/server/handler.js +44 -6
- package/server/presentations.js +31 -2
- package/server/publier.js +72 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.56",
|
|
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
|
@@ -309,6 +309,12 @@ var Live=(function(){
|
|
|
309
309
|
// participants, d'URL — donc du dehors. typers est le cas vif : il est alimenté par 'typing',
|
|
310
310
|
// le SEUL événement qui croie encore son émetteur (cf. 0.1.30). Un objet nu retire la question
|
|
311
311
|
// entière au lieu de la traiter cas par cas. (audit P1-2)
|
|
312
|
+
// ⚠️ _ordEtat, _ordChat et _filet VIVAIENT DANS connect() : disconnect(), défini ici, ne pouvait
|
|
313
|
+
// pas les atteindre. Après connect → disconnect → connect, les relectures de l'ancienne session
|
|
314
|
+
// continuaient — un spectateur qui rouvre la page doublait le trafic, et le filet de sécurité
|
|
315
|
+
// battait deux fois. On les déclare au niveau du CYCLE DE VIE : ce qui doit être arrêté doit
|
|
316
|
+
// être visible depuis l'endroit qui arrête.
|
|
317
|
+
var _ordEtat=null,_ordChat=null,_filet=null;
|
|
312
318
|
var sb=null,ch=null,ME=null,SLUG=null,CONTROL=null,LOCKED=false,AUTHTOK=null,PRESENT=[],PRESNAME='',seen=Object.create(null),msgEls=Object.create(null),msgData=Object.create(null),replyCtx=null,typers=Object.create(null),pdfCache=Object.create(null),_tyT=0,_tyIv=0,_atIv=0,unread=0,autoOpened=false,_histDone=false,_phWired=false,_onMap=null,_onState=null,_peekT=0,MUTED=false;
|
|
313
319
|
try{ MUTED=localStorage.getItem('3dd-present-mute')==='1'; }catch(e){}
|
|
314
320
|
// Couper/rétablir les notifications du chat (cloche) : coupé = plus de ticker ni de pulse (badge silencieux gardé).
|
|
@@ -419,7 +425,14 @@ var Live=(function(){
|
|
|
419
425
|
if(y)y.onclick=function(){close();if(onOk)onOk();};if(n)n.onclick=close;m.onclick=function(e){if(e.target===m)close();};
|
|
420
426
|
document.addEventListener('keydown',key);m.classList.add('open');if(y)try{y.focus();}catch(e){}}
|
|
421
427
|
function history(){fetch('/api/doc?present='+encodeURIComponent(SLUG)+'&chat=1').then(function(r){return r.json();}).then(function(d){var box=document.getElementById('chatMsgs');if(d&&d.messages&&d.messages.length){d.messages.forEach(function(m){addMsg(m);});}else if(box&&!box.children.length){box.innerHTML='<div class=chat-empty>Aucun message. Lancez la discussion.</div>';}if(d&&typeof d.locked!=='undefined')applyLock(d.locked);_histDone=true;}).catch(function(){_histDone=true;});}
|
|
422
|
-
function react(id,e){if(!ME||!id||!e)return;
|
|
428
|
+
function react(id,e){if(!ME||!id||!e)return;
|
|
429
|
+
// ⚠️ ON ENVOIE L'ÉTAT VOULU, PAS « INVERSE ». Basculer n'a de sens qu'une fois : un renvoi
|
|
430
|
+
// réseau, un double-clic, une reprise de requête, et la réaction que le participant vient
|
|
431
|
+
// d'ajouter disparaît — sans aucune erreur affichée. Il voit son émoji s'allumer puis
|
|
432
|
+
// s'éteindre, recommence, et rebascule encore. Rejouer la même intention deux fois donne le
|
|
433
|
+
// même résultat qu'une fois ; c'est ce que le réseau exige.
|
|
434
|
+
var _m=msgData[id]||{},_rs=(_m.reactions&&_m.reactions[e])||[],veut=_rs.indexOf(MOIREF)<0;
|
|
435
|
+
fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'present-react',slug:SLUG,msgId:+id,emoji:e,reactor:MOIREF,etat:veut})}).then(majDiffusee).catch(function(){});}
|
|
423
436
|
function setReply(id){var m=msgData[id];if(!m||m.deleted)return;var nm=m.author_name||'Invité';replyCtx={id:+id,name:nm,text:(m.body||'').slice(0,120)};var el=document.getElementById('chatReply');if(el){el.style.display='flex';el.innerHTML='<span class=cq><b>'+esc(nm)+'</b> '+esc((m.body||'').slice(0,80))+'</span><button id=chatReplyX title=Annuler>×</button>';var x=document.getElementById('chatReplyX');if(x)x.addEventListener('click',clearReply);}var t=document.getElementById('chatText');if(t)t.focus();}
|
|
424
437
|
function clearReply(){replyCtx=null;var el=document.getElementById('chatReply');if(el){el.style.display='none';el.innerHTML='';}}
|
|
425
438
|
function send(){var i=document.getElementById('chatText');var t=(i.value||'').trim();if(!t||!ME)return;if(LOCKED&&!canMod())return;i.value='';toggleSend();
|
|
@@ -605,14 +618,14 @@ var Live=(function(){
|
|
|
605
618
|
arreter:function(){ ord.arreter(); },
|
|
606
619
|
};
|
|
607
620
|
}
|
|
608
|
-
|
|
621
|
+
_ordEtat=relireAvec('&state=1',function(d){if(d.state)etatDuServeur(d.state);});
|
|
609
622
|
// ⚠️ Exposé hors de cette fermeture : la carte vit dans un AUTRE bloc de script et doit
|
|
610
623
|
// pouvoir déclencher la relecture. Même raison que window.__presAppliquerEtat — un nom
|
|
611
624
|
// référencé depuis la mauvaise portée part dans un catch muet, et l'audience se fige.
|
|
612
625
|
window.__presRelireEtat=function(){_ordEtat.signaler();};
|
|
613
626
|
function relireEtat(){_ordEtat.signaler();}
|
|
614
627
|
function relireChat(){_ordChat.signaler();}
|
|
615
|
-
|
|
628
|
+
_ordChat=relireAvec('&chat=1',function(d){
|
|
616
629
|
// ⚠️ NOTIFIER CE QUI VIENT D'ARRIVER, ET SEULEMENT ÇA. 'addMsg' rend faux pour un
|
|
617
630
|
// message déjà connu — la relecture ramène tout l'historique, donc sans cette condition
|
|
618
631
|
// la pastille de non-lus compterait chaque message à chaque relecture. Et sans l'appel,
|
|
@@ -627,7 +640,7 @@ var Live=(function(){
|
|
|
627
640
|
// rattrape ce cas : elle ne coûte presque rien et évite qu'une audience reste figée sur un
|
|
628
641
|
// état périmé en croyant être à jour. C'est l'inverse d'une optimisation : c'est le prix de
|
|
629
642
|
// la borne.
|
|
630
|
-
|
|
643
|
+
_filet=setInterval(function(){_ordEtat.maintenant();_ordChat.maintenant();},25000);
|
|
631
644
|
|
|
632
645
|
// ⚠️ LE TITRE VIENT D'ICI, PAS DE LA PRÉSENCE. La liste des participants tirait
|
|
633
646
|
// « présentateur » de la charge de présence, que chacun compose lui-même : un
|
|
@@ -662,7 +675,7 @@ var Live=(function(){
|
|
|
662
675
|
// → évite les fantômes (« je me vois deux fois » au retour). Une seule fois.
|
|
663
676
|
if(!_phWired){_phWired=true;window.addEventListener('pagehide',function(){try{clearInterval(_filet);_ordEtat.arreter();_ordChat.arreter();}catch(e){}try{if(ch){ch.untrack();ch.unsubscribe();ch=null;}}catch(e){}});}
|
|
664
677
|
}catch(e){}}
|
|
665
|
-
function disconnect(){try{clearInterval(_tyIv);}catch(e){}try{clearInterval(_atIv);}catch(e){}try{sendAttend();}catch(e){}try{if(ch){ch.untrack();ch.unsubscribe();ch=null;}}catch(e){}var pb=document.getElementById('presBtn');if(pb)pb.style.display='none';var cb=document.getElementById('chatBtn');if(cb)cb.style.display='none';var _fb=document.getElementById('chatFab');if(_fb)_fb.classList.remove('on');var pn=document.getElementById('chatPanel');if(pn)pn.classList.add('hidden');}
|
|
678
|
+
function disconnect(){try{clearInterval(_tyIv);}catch(e){}try{clearInterval(_atIv);}catch(e){}try{clearInterval(_filet);_filet=null;}catch(e){}try{if(_ordEtat)_ordEtat.arreter();_ordEtat=null;}catch(e){}try{if(_ordChat)_ordChat.arreter();_ordChat=null;}catch(e){}try{delete window.__presRelireEtat;}catch(e){window.__presRelireEtat=null;}try{sendAttend();}catch(e){}try{if(ch){ch.untrack();ch.unsubscribe();ch=null;}}catch(e){}var pb=document.getElementById('presBtn');if(pb)pb.style.display='none';var cb=document.getElementById('chatBtn');if(cb)cb.style.display='none';var _fb=document.getElementById('chatFab');if(_fb)_fb.classList.remove('on');var pn=document.getElementById('chatPanel');if(pn)pn.classList.add('hidden');}
|
|
666
679
|
// Membre de l'équipe reconnu via la session app (MÊME ORIGINE, localStorage) → avatar + nom auto.
|
|
667
680
|
// Le jeton d'acces de la session locale, quand il y en a une. C'est la SEULE chose qui prouve au
|
|
668
681
|
// serveur qu'on est un membre ; 'member:true' dans la page ne prouve rien, il ne sert qu'a l'affichage.
|
|
@@ -1532,6 +1545,12 @@ async function profilDuJeton(req) {
|
|
|
1532
1545
|
} catch { return null; }
|
|
1533
1546
|
}
|
|
1534
1547
|
|
|
1548
|
+
/**
|
|
1549
|
+
* Plafond du relais, en octets. Réglable par l'exploitant : un hôte qui sert des plans
|
|
1550
|
+
* d'architecte n'a pas les mêmes documents qu'un hôte qui sert des notices.
|
|
1551
|
+
*/
|
|
1552
|
+
const PLAFOND_RELAIS = Number(process.env.PLAYER_MAX_RELAY_BYTES || 0) || 60 * 1024 * 1024;
|
|
1553
|
+
|
|
1535
1554
|
async function relayerFichier(res, r, disposition) {
|
|
1536
1555
|
if (!r) { res.statusCode = 404; res.end("Fichier indisponible"); return; }
|
|
1537
1556
|
if (!r.ok && r.status !== 206) { res.statusCode = 502; res.end("Fichier indisponible"); return; }
|
|
@@ -1539,6 +1558,23 @@ async function relayerFichier(res, r, disposition) {
|
|
|
1539
1558
|
const compresse = !!r.headers.get("content-encoding");
|
|
1540
1559
|
if (compresse && r.status === 206) { res.statusCode = 502; res.end("Fichier indisponible"); return; }
|
|
1541
1560
|
|
|
1561
|
+
// ⚠️ UN PLAFOND, ET IL REFUSE AVANT D'ALLOUER. Ce relais chargeait le fichier ENTIER en mémoire
|
|
1562
|
+
// sans borne : un PDF de 80 Mo, trois requêtes Range simultanées, et une fonction serverless
|
|
1563
|
+
// tombe — pas pour un document en particulier, pour la somme. Le défaut n'est pas la taille d'un
|
|
1564
|
+
// fichier, c'est l'absence de toute limite haute.
|
|
1565
|
+
//
|
|
1566
|
+
// ⚠️ ON REGARDE `Content-Length` AVANT DE LIRE LE CORPS : refuser après l'allocation ne protège
|
|
1567
|
+
// de rien, c'est l'allocation qui coûte. Un amont qui n'annonce pas sa taille passe quand même —
|
|
1568
|
+
// on ne peut pas refuser ce qu'on ne sait pas mesurer, et fermer par défaut couperait des
|
|
1569
|
+
// stockages parfaitement légitimes. La borne est donc une garde contre le GROS, pas contre
|
|
1570
|
+
// l'inconnu ; c'est ce que le streaming, lui, fermera vraiment.
|
|
1571
|
+
const annoncee = Number(r.headers.get("content-length") || 0);
|
|
1572
|
+
if (annoncee > PLAFOND_RELAIS) {
|
|
1573
|
+
try { PLAYER.errors.capture(new Error(`relais refusé : ${annoncee} octets au-dessus du plafond de ${PLAFOND_RELAIS}`), { route: "relais" }); } catch { /* jamais bloquant */ }
|
|
1574
|
+
res.statusCode = 413;
|
|
1575
|
+
res.end("Fichier trop volumineux");
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1542
1578
|
const buf = Buffer.from(await r.arrayBuffer());
|
|
1543
1579
|
res.statusCode = r.status;
|
|
1544
1580
|
const typeAmont = r.headers.get("content-type") || "application/pdf";
|
|
@@ -4028,6 +4064,8 @@ async function handler(req, res) {
|
|
|
4028
4064
|
// ⚠️ `TIERS` est exporté pour être CONFRONTÉ, pas pour être utilisé. Le banc navigateur et la
|
|
4029
4065
|
// forge doivent pouvoir demander « quelles dépendances tierces, à quelles empreintes » sans en
|
|
4030
4066
|
// tenir une seconde liste — c'est la seule façon qu'une empreinte périmée finisse par se voir.
|
|
4031
|
-
|
|
4067
|
+
// ⚠️ Exporté pour être ÉPROUVÉ, pas pour être appelé : le plafond du relais ne se vérifie qu en
|
|
4068
|
+
// regardant si le corps a été lu, ce qu aucune route ne peut montrer de l extérieur.
|
|
4069
|
+
module.exports = { handler, init, TIERS, __relayerFichier: relayerFichier };
|
|
4032
4070
|
|
|
4033
4071
|
// redeploy: forcer le build production (Vercel a sauté la prod du merge #463 — wording re-partage).
|
package/server/presentations.js
CHANGED
|
@@ -381,6 +381,19 @@ function messagePublic(row) {
|
|
|
381
381
|
if ("reactions" in out) out.reactions = reactionsPubliques(out.reactions);
|
|
382
382
|
// Dérivé, jamais recopié : `author_hash` est lu pour ça et ne sort jamais tel quel.
|
|
383
383
|
if ("author_hash" in row) out.author_ref = refAuteur(row.author_hash);
|
|
384
|
+
// ⚠️ LA GARDE CATÉGORIELLE N'EST PAS ICI, ET LA RAISON EST STRUCTURELLE.
|
|
385
|
+
//
|
|
386
|
+
// La liste blanche s'exécute AVANT : elle a déjà retiré tout champ inconnu. `publier()` n'y
|
|
387
|
+
// verrait donc qu'un jeu de champs connus, et aucune mutation ne peut la faire tomber à cet
|
|
388
|
+
// endroit — la redondance n'est pas une ceinture, c'est une garde qu'on ne peut pas éprouver.
|
|
389
|
+
//
|
|
390
|
+
// ⚠️ Et le cas qu'on écrirait pour forcer le rouge serait faux : `author_name` est CHOISI par le
|
|
391
|
+
// participant. Quelqu'un qui se nomme « lea@exemple.fr » verrait le chat tomber — le piège de
|
|
392
|
+
// `body`, rencontré une seconde fois au même endroit.
|
|
393
|
+
//
|
|
394
|
+
// Sa place est aux sorties SANS liste blanche : charge de présence, statistiques, aperçu d'un
|
|
395
|
+
// partage — là où un champ ajouté demain sort sans que personne ne l'ait décidé. C'est là qu'une
|
|
396
|
+
// mutation pourra rougir, et donc là qu'elle sera branchée.
|
|
384
397
|
return out;
|
|
385
398
|
}
|
|
386
399
|
|
|
@@ -470,7 +483,21 @@ async function setChatLock(slug, control, locked) {
|
|
|
470
483
|
}
|
|
471
484
|
|
|
472
485
|
// Réaction emoji (toggle) : le participant (identifié par email ou nom) ajoute/retire un emoji sur un message.
|
|
473
|
-
|
|
486
|
+
/**
|
|
487
|
+
* ⚠️ ON POSE UN ÉTAT, ON NE BASCULE PLUS — et la différence est ce qui rend l'opération IDEMPOTENTE.
|
|
488
|
+
*
|
|
489
|
+
* « Basculer » n'a de sens qu'une fois : un renvoi réseau, un double-clic, une reprise de requête,
|
|
490
|
+
* et la réaction que le participant vient d'ajouter disparaît. Il ne voit aucune erreur — il voit
|
|
491
|
+
* son émoji s'allumer puis s'éteindre, et il recommence, ce qui rebascule encore.
|
|
492
|
+
*
|
|
493
|
+
* L'appelant sait ce qu'il VEUT (`etat`), pas ce qu'il faut inverser. Rejouer la même intention
|
|
494
|
+
* deux fois donne le même résultat qu'une fois, ce qui est exactement la propriété qu'un réseau
|
|
495
|
+
* peu fiable exige. Constat P10 de l'audit.
|
|
496
|
+
*
|
|
497
|
+
* ⚠️ Compatibilité : `etat` absent = ancien client, on bascule comme avant. Un client à jour ne
|
|
498
|
+
* repasse jamais par là ; un ancien garde son comportement plutôt que de perdre la fonction.
|
|
499
|
+
*/
|
|
500
|
+
async function toggleReaction(slug, msgId, emoji, reactor, etat) {
|
|
474
501
|
if (await estArchive(slug)) return REFUS_ARCHIVE;
|
|
475
502
|
const id = Math.trunc(+msgId); const e = String(emoji || "").slice(0, 8); const who = String(reactor || "").slice(0, 160).toLowerCase();
|
|
476
503
|
if (!id || !e || !who) return { ok: false, status: 400 };
|
|
@@ -498,7 +525,9 @@ async function toggleReaction(slug, msgId, emoji, reactor) {
|
|
|
498
525
|
const cur = Object.assign(Object.create(null), brut);
|
|
499
526
|
const arr = Array.isArray(cur[e]) ? cur[e] : [];
|
|
500
527
|
const i = arr.indexOf(who);
|
|
501
|
-
|
|
528
|
+
const veut = etat === undefined || etat === null ? (i < 0) : !!etat;
|
|
529
|
+
if (veut && i < 0) arr.push(who);
|
|
530
|
+
if (!veut && i >= 0) arr.splice(i, 1);
|
|
502
531
|
if (arr.length) cur[e] = arr; else delete cur[e];
|
|
503
532
|
const maj = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&select=*`, { method: "PATCH", headers: { Prefer: "return=representation" }, body: { reactions: cur } });
|
|
504
533
|
return { ok: true, message: premierPublic(maj) };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// AUCUNE ADRESSE NE SORT — LA CATÉGORIE, PAS LES QUATRE CHEMINS CONNUS.
|
|
2
|
+
//
|
|
3
|
+
// Nous avions corrigé quatre chemins qui portaient l'adresse d'un membre : les champs publics du
|
|
4
|
+
// chat, la carte des réactions, la charge de présence, la clé du canal. Le second hôte a dit ce
|
|
5
|
+
// qui manquait, et il avait raison : « mon fermé vaut pour les chemins que j'ai su nommer, pas
|
|
6
|
+
// pour la catégorie ». Rien ne garantissait qu'il n'y en avait pas un cinquième.
|
|
7
|
+
//
|
|
8
|
+
// ⚠️ CE N'EST PAS UNE SONDE QUI PRODUIT UNE LISTE. Une garde qui signale « ces vingt endroits
|
|
9
|
+
// ressemblent à des adresses » transfère le travail : quelqu'un doit juger vingt cas, et le
|
|
10
|
+
// jugement est précisément ce qui ne passe pas à l'échelle. Celle-ci REFUSE — le champ ne sort
|
|
11
|
+
// pas, et il faut écrire une décision pour qu'il sorte.
|
|
12
|
+
//
|
|
13
|
+
// ⚠️ ET ELLE NE NETTOIE PAS EN SILENCE. Effacer l'adresse au passage serait pire que la laisser :
|
|
14
|
+
// on croirait la donnée partie alors qu'elle n'a jamais été prévue. Une valeur inattendue à la
|
|
15
|
+
// sortie est un défaut de conception, pas une impureté à filtrer — donc on lève.
|
|
16
|
+
|
|
17
|
+
// Reconnaît une adresse par sa FORME, pas par le nom du champ. `author_email` était nommé ; les
|
|
18
|
+
// réactions ne l'étaient pas, et c'est là que l'adresse se cachait.
|
|
19
|
+
const ADRESSE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Rend l'objet tel quel s'il ne porte aucune adresse ; lève sinon, en NOMMANT le chemin.
|
|
23
|
+
*
|
|
24
|
+
* Le chemin compte plus que la valeur : « reactions.👍[0] » dit où regarder, « une adresse a été
|
|
25
|
+
* trouvée » envoie chercher.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* ⚠️ LES CHAMPS QUE LES GENS RÉDIGENT, ET CEUX QUI DÉSIGNENT QUELQU'UN — la distinction que le
|
|
29
|
+
* premier branchement n'avait pas.
|
|
30
|
+
*
|
|
31
|
+
* Branchée sur tout, la garde levait sur `body` : un participant qui écrit « écris-moi à
|
|
32
|
+
* lea@exemple.fr » dans le chat voyait son message provoquer une exception. Elle transformait un
|
|
33
|
+
* usage normal en panne, et je l'ai mesuré plutôt que supposé.
|
|
34
|
+
*
|
|
35
|
+
* Une adresse DANS UN MESSAGE est du contenu : son auteur l'a écrite exprès, pour un destinataire
|
|
36
|
+
* qu'il a choisi. Une adresse dans un champ d'IDENTITÉ n'a été choisie par personne : elle vient
|
|
37
|
+
* de la base, et elle sort vers toute l'audience. Ce n'est pas la même donnée, et la garde ne doit
|
|
38
|
+
* pas les traiter pareil.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ LISTE DES CHAMPS DE CONTENU, PAS DES CHAMPS D'IDENTITÉ — et le sens de l'inversion est la
|
|
41
|
+
* garde elle-même. Énumérer ce qu'on protège laisse passer la colonne ajoutée demain ; énumérer
|
|
42
|
+
* ce qu'on EXEMPTE oblige à écrire une décision pour chaque exemption. Un champ inconnu est
|
|
43
|
+
* protégé par défaut.
|
|
44
|
+
*/
|
|
45
|
+
const CONTENU = new Set(["body", "reply_text", "text", "message", "content", "caption"]);
|
|
46
|
+
|
|
47
|
+
function publier(valeur, ou = "") {
|
|
48
|
+
if (typeof valeur === "string") {
|
|
49
|
+
if (ADRESSE.test(valeur)) {
|
|
50
|
+
throw Object.assign(
|
|
51
|
+
new Error(`adresse refusée à la sortie publique : ${ou || "(racine)"}`),
|
|
52
|
+
{ statusCode: 500, champ: ou },
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return valeur;
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(valeur)) { valeur.forEach((v, i) => publier(v, `${ou}[${i}]`)); return valeur; }
|
|
58
|
+
if (valeur && typeof valeur === "object") {
|
|
59
|
+
// ⚠️ Les CLÉS aussi : la carte des réactions portait les identités en clé chez d'autres hôtes,
|
|
60
|
+
// et une garde qui ne regarde que les valeurs laisserait passer exactement ce cas.
|
|
61
|
+
for (const [k, v] of Object.entries(valeur)) {
|
|
62
|
+
publier(k, ou ? `${ou}.<clé>` : "<clé>");
|
|
63
|
+
// Le champ que quelqu'un a rédigé traverse ; tout le reste est inspecté.
|
|
64
|
+
if (CONTENU.has(k)) continue;
|
|
65
|
+
publier(v, ou ? `${ou}.${k}` : k);
|
|
66
|
+
}
|
|
67
|
+
return valeur;
|
|
68
|
+
}
|
|
69
|
+
return valeur;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { publier, ADRESSE, CONTENU };
|