openvibers/openvibe.tools Commits

Dev tools run on the network's web runtime; the webhook viewer shows a sender's method and headers as text (roadmap WS-P task 6, the second site; openvibe-shared 1.22.0 -> 1.23.0 in every app). dev.html loads /shared/web-runtime.js (the gateway serves it from its pin) and makes each Run a route generation: a new Run or Clear ends the previous one's work. Before, every Run of the webhook tool left a 2 s poller running for an hour (the pollers piled up), a slow earlier result could overwrite a newer one, and two quick minifier runs added Terser's script twice. Now the bin's poller is a child of the run's scope (stops on the next Run or Clear, an hour at most) and polls with the run's AbortSignal, a stale run never writes the output, and Terser loads once through the runtime. Security (low/medium, stored XSS): the webhook viewer inserted each captured request's method and header names and values into innerHTML unescaped (only the body was escaped), so whoever sends to a bin URL could run script on *.openvibe.tools, where ov_token sits in localStorage; they are escaped now. apps/gateway/test/dev-runtime.test.js runs the page's script in a vm: one poller after repeated runs, the aborted request, Clear, a hostile method and header shown as text, one Terser tag. Checked in headless Chrome against a local gateway: two runs at once, one Terser tag, the right output, RT.leaks() empty. npm test 51/51.

OP OpenVibers committed 27 Sept 2026 · 68f13302b3 · 18 files, +170 −49


Changes

ddiff --git a/apps/audio/package-lock.json b/apps/audio/package-lock.json
iindex aeeb5b7..ad2148c 100644
---- a/apps/audio/package-lock.json
++++ b/apps/audio/package-lock.json
@@@ -21,7 +21,7 @@
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
"uuid": "^11.1.0"
}
},
@@@ -1184,9 +1184,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "1.22.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
-- "integrity": "sha512-FtqbR8h6YphOnJNqklJED3HyqvTwZJl58gyiZ3QgzmZtZcA16O2NMGq67O4p7grLojwaIG5DhXDMgmZcpScxJQ==",
++ "version": "1.23.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
++ "integrity": "sha512-9B3vXn5P7JjHWrp3cNh7ZynLOmCUgw9cuz6moksU3fyuIfy7uoepa2wJmQeSyLoPU8q/zKRJVeBVzPfdhr0z3g==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/apps/audio/package.json b/apps/audio/package.json
iindex ab96e5a..9a4b4f3 100644
---- a/apps/audio/package.json
++++ b/apps/audio/package.json
@@@ -23,7 +23,7 @@
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
"uuid": "^11.1.0"
}
}
ddiff --git a/apps/docs/package-lock.json b/apps/docs/package-lock.json
iindex b0ba79f..14a60d9 100644
---- a/apps/docs/package-lock.json
++++ b/apps/docs/package-lock.json
@@@ -20,7 +20,7 @@
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
"pdf-lib": "^1.17.1",
"sharp": "^0.33.5",
"uuid": "^11.1.0"
@@@ -1263,9 +1263,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "1.22.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
-- "integrity": "sha512-FtqbR8h6YphOnJNqklJED3HyqvTwZJl58gyiZ3QgzmZtZcA16O2NMGq67O4p7grLojwaIG5DhXDMgmZcpScxJQ==",
++ "version": "1.23.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
++ "integrity": "sha512-9B3vXn5P7JjHWrp3cNh7ZynLOmCUgw9cuz6moksU3fyuIfy7uoepa2wJmQeSyLoPU8q/zKRJVeBVzPfdhr0z3g==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/apps/docs/package.json b/apps/docs/package.json
iindex 24d6355..c183bff 100644
---- a/apps/docs/package.json
++++ b/apps/docs/package.json
@@@ -22,7 +22,7 @@
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
"pdf-lib": "^1.17.1",
"sharp": "^0.33.5",
"uuid": "^11.1.0"
ddiff --git a/apps/food/package-lock.json b/apps/food/package-lock.json
iindex 0e242ce..32c5052 100644
---- a/apps/food/package-lock.json
++++ b/apps/food/package-lock.json
@@@ -15,7 +15,7 @@
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0"
}
},
"node_modules/accepts": {
@@@ -968,9 +968,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "1.22.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
-- "integrity": "sha512-FtqbR8h6YphOnJNqklJED3HyqvTwZJl58gyiZ3QgzmZtZcA16O2NMGq67O4p7grLojwaIG5DhXDMgmZcpScxJQ==",
++ "version": "1.23.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
++ "integrity": "sha512-9B3vXn5P7JjHWrp3cNh7ZynLOmCUgw9cuz6moksU3fyuIfy7uoepa2wJmQeSyLoPU8q/zKRJVeBVzPfdhr0z3g==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/apps/food/package.json b/apps/food/package.json
iindex 098f977..7b725b3 100644
---- a/apps/food/package.json
++++ b/apps/food/package.json
@@@ -17,6 +17,6 @@
"better-sqlite3": "^11.0.0",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0"
}
}
ddiff --git a/apps/gateway/package-lock.json b/apps/gateway/package-lock.json
iindex 5fabd91..a079010 100644
---- a/apps/gateway/package-lock.json
++++ b/apps/gateway/package-lock.json
@@@ -19,7 +19,7 @@
"jsonwebtoken": "^9.0.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0"
}
},
"node_modules/accepts": {
@@@ -1061,9 +1061,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "1.22.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
-- "integrity": "sha512-FtqbR8h6YphOnJNqklJED3HyqvTwZJl58gyiZ3QgzmZtZcA16O2NMGq67O4p7grLojwaIG5DhXDMgmZcpScxJQ==",
++ "version": "1.23.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
++ "integrity": "sha512-9B3vXn5P7JjHWrp3cNh7ZynLOmCUgw9cuz6moksU3fyuIfy7uoepa2wJmQeSyLoPU8q/zKRJVeBVzPfdhr0z3g==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/apps/gateway/package.json b/apps/gateway/package.json
iindex 51605bc..170444d 100644
---- a/apps/gateway/package.json
++++ b/apps/gateway/package.json
@@@ -21,6 +21,6 @@
"jsonwebtoken": "^9.0.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0"
}
}
ddiff --git a/apps/gateway/public/dev.html b/apps/gateway/public/dev.html
iindex 3ceffb6..5ae0c52 100644
---- a/apps/gateway/public/dev.html
++++ b/apps/gateway/public/dev.html
@@@ -147,6 +147,7 @@ a{color:inherit;text-decoration:none}
<script src="https://openvibe.network/shared/navbar.js"></script>
<script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/theme-loader.js" defer></script>
++<script src="/shared/web-runtime.js"></script>
<script src="/js/dev-engine.js"></script>
<script>
'use strict';
@@@ -262,10 +263,14 @@ function buildRel(){
}
// ═══════════════ PROCESSORS ═══════════════
++// Each run is a generation of the network's web runtime (openvibe-shared/web-runtime): a new run (or Clear) ends
++// the previous one's pollers and requests, and a slow earlier result never overwrites a newer one.
++const RT=OVWebRuntime.create({});
async function run(){
++ const g=RT.nextRoute(),sc=RT.scope();
const i1=$('in1')?.value||'',i2=$('in2')?.value||'',o=$('out');
-- try{const r=await P[ACT](i1,MODE,i2);if(r==null)return;o.innerHTML=typeof r==='object'?r.html:H(String(r))}
-- catch(e){o.innerHTML='<span class="err">\u2717 '+H(e.message)+'</span>'}
++ try{const r=await P[ACT](i1,MODE,i2,sc);if(r==null||!RT.isCurrent(g))return;o.innerHTML=typeof r==='object'?r.html:H(String(r))}
++ catch(e){if(RT.isCurrent(g)&&e.name!=='AbortError')o.innerHTML='<span class="err">\u2717 '+H(e.message)+'</span>'}
}
const P={};
@@@ -312,7 +317,7 @@ P.cron=(s)=>DevEngine.cron(s);
P.beautify=(s,m)=>DevEngine.beautify(s,m);
// ── Single-purpose tools ─────────────────────────────────────
--const loadScript=(src)=>new Promise((ok,no)=>{const e=document.createElement('script');e.src=src;e.onload=ok;e.onerror=()=>no(new Error('Could not load the minifier. Check your connection and try again.'));document.head.appendChild(e)});
++const loadScript=(src)=>RT.loadScript(src).catch(()=>{throw new Error('Could not load the minifier. Check your connection and try again.')});
const terser=async()=>{if(!window.Terser)await loadScript('https://cdn.jsdelivr.net/npm/terser@5/dist/bundle.min.js');return window.Terser};
const saved=(a,b)=>{const x=new Blob([a]).size,y=new Blob([b]).size;return x?`${x.toLocaleString()} → ${y.toLocaleString()} bytes (${Math.max(0,Math.round((1-y/x)*100))}% smaller)`:''};
const withNote=(code,note)=>({html:(note?'<div style="color:var(--tx2);font-size:.8rem;margin-bottom:8px">'+H(note)+'</div>':'')+'<pre style="margin:0;white-space:pre-wrap;word-break:break-word">'+H(code)+'</pre>'});
@@@ -377,14 +382,15 @@ P.lorem=(s,m)=>DevEngine.lorem(m.includes('Words')?'words':m.includes('Sent')?'s
P.curlconvert=(s,m)=>DevEngine.curlconvert(s,m);
--P.webhook=async()=>{
++P.webhook=async(s,m,s2,sc)=>{
const o=$('out');
-- const r=await fetch(API+'/webhook/bins',{method:'POST',credentials:'include'});const d=await r.json();if(!d.ok)throw new Error(d.error||'Failed');
++ const r=await sc.fetch(API+'/webhook/bins',{method:'POST',credentials:'include'});const d=await r.json();if(!d.ok)throw new Error(d.error||'Failed');
const binUrl=API+'/webhook/bins/'+d.binId+'/in';
o.innerHTML='<div style="margin-bottom:12px"><div class="jl">Your Webhook URL</div><div style="font-size:1rem;word-break:break-all;color:var(--ac);cursor:pointer" onclick="navigator.clipboard.writeText(\''+binUrl+"');toast('Copied!')\">"+binUrl+'</div><div style="color:var(--tx3);font-size:.78rem;margin-top:4px">Click to copy \u00b7 Polling for requests...</div></div><div id="whr">Waiting for requests...</div>';
-- const iv=setInterval(async()=>{try{const r2=await fetch(API+'/webhook/bins/'+d.binId,{credentials:'include'});const d2=await r2.json();
-- if(d2.ok&&d2.requests.length){$('whr').innerHTML=d2.requests.map((rq,i)=>'<div class="js"><div class="jl">#'+(i+1)+' '+rq.method+' \u00b7 '+new Date(rq.timestamp).toLocaleTimeString()+'</div>Headers:\n'+Object.entries(rq.headers||{}).map(([k,v])=>k+': '+v).join('\n')+'\n\nBody:\n'+H(typeof rq.body==='object'?JSON.stringify(rq.body,null,2):String(rq.body||'(empty)'))+'</div>').join('')}}catch{}},2000);
-- setTimeout(()=>clearInterval(iv),3600000);return null;
++ const poll=sc.child(); // polls until the next run or Clear, for an hour at most
++ poll.interval(async()=>{try{const r2=await poll.fetch(API+'/webhook/bins/'+d.binId,{credentials:'include'});const d2=await r2.json();
++ if(d2.ok&&d2.requests.length){$('whr').innerHTML=d2.requests.map((rq,i)=>'<div class="js"><div class="jl">#'+(i+1)+' '+H(String(rq.method||''))+' \u00b7 '+H(new Date(rq.timestamp).toLocaleTimeString())+'</div>Headers:\n'+Object.entries(rq.headers||{}).map(([k,v])=>H(k)+': '+H(String(v))).join('\n')+'\n\nBody:\n'+H(typeof rq.body==='object'?JSON.stringify(rq.body,null,2):String(rq.body||'(empty)'))+'</div>').join('')}}catch{}},2000);
++ poll.timeout(()=>poll.dispose(),3600000);return null;
};
P.color=(s)=>{
@@@ -420,7 +426,7 @@ async function saveAsPaste(btn){
toast('Saved as a paste');
}catch(e){toast(e.message)}finally{btn.disabled=false}
}
--function clr(){const p=$('pasted');if(p)p.remove();const a=$('in1');if(a)a.value='';const b=$('in2');if(b)b.value='';$('out').innerHTML=''}
++function clr(){RT.nextRoute();const p=$('pasted');if(p)p.remove();const a=$('in1');if(a)a.value='';const b=$('in2');if(b)b.value='';$('out').innerHTML=''}
function sample(){const t=T[ACT];if(t?.p){$('in1').value=t.p;if(t.p2&&$('in2'))$('in2').value=t.p2}run()}
function toast(msg){const t=$('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2000)}
</script>
ddiff --git a/apps/gateway/test/dev-runtime.test.js b/apps/gateway/test/dev-runtime.test.js
nnew file mode 100644
iindex 0000000..3be0f2b
---- /dev/null
++++ b/apps/gateway/test/dev-runtime.test.js
@@@ -0,0 +1,115 @@
++'use strict';
++// dev.html on the network's web runtime (openvibe-shared/web-runtime, roadmap WS-P task 6). The page's own script runs
++// in a vm with a small fake DOM, timers and fetch:
++// - each Run is a route generation: running the webhook tool again stops the previous bin's poller and aborts its
++// request, and Clear stops the poller (before, every Run left a 2 s poller running for an hour);
++// - two quick runs of a minifier add Terser's <script> once;
++// - a webhook request's method and headers are shown as text (a sender's markup never becomes the page's).
++// node apps/gateway/test/dev-runtime.test.js
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const vm = require('vm');
++
++const html = fs.readFileSync(path.join(__dirname, '..', 'public', 'dev.html'), 'utf8');
++const runtimeTag = html.indexOf('<script src="/shared/web-runtime.js"></script>');
++assert.ok(runtimeTag > 0 && runtimeTag < html.indexOf("const API='https://openvibe.tools/api/dev'"), 'dev.html loads the web runtime before its own script');
++assert.ok(require('openvibe-shared/files').isBrowserFile('web-runtime.js'), 'the pinned openvibe-shared serves web-runtime.js at /shared/');
++const start = html.indexOf("<script>\n'use strict';\nconst API=");
++const PAGE = html.slice(start + '<script>'.length, html.indexOf('</script>', start));
++assert.ok(!/\bsetInterval\(/.test(PAGE), 'no page poller outside a run\'s scope');
++
++function page(host) {
++ const els = new Map();
++ const el = (id) => {
++ if (id === 'pasted') return null;
++ if (!els.has(id)) els.set(id, { id, innerHTML: '', value: '', textContent: '', style: {}, classList: { add() {}, remove() {} }, remove() {} });
++ return els.get(id);
++ };
++ const scripts = [];
++ const timers = new Map(); let tid = 0;
++ const requests = [];
++ const bins = [];
++ const ctx = {
++ console, JSON, Promise, Date, Math, Object, Array, Error, URL, URLSearchParams, AbortController, TextEncoder, Blob,
++ location: { hostname: host, search: '', href: `https://${host}/`, origin: `https://${host}` },
++ navigator: {}, localStorage: { getItem: () => null, setItem() {} },
++ CustomEvent: class { constructor(t, o) { this.type = t; this.detail = o && o.detail; } },
++ document: {
++ getElementById: el, querySelector: () => null, querySelectorAll: () => [], addEventListener() {}, dispatchEvent() {},
++ head: { appendChild: (n) => { scripts.push(n); return n; } },
++ createElement: (tag) => { const n = { tagName: tag.toUpperCase(), dataset: {}, setAttribute() {}, remove() { n.removed = true; } }; return n; },
++ },
++ setTimeout: (f, ms) => { timers.set(++tid, { f, ms, kind: 'timeout' }); return tid; },
++ clearTimeout: (id) => { timers.delete(id); },
++ setInterval: (f, ms) => { timers.set(++tid, { f, ms, kind: 'interval' }); return tid; },
++ clearInterval: (id) => { timers.delete(id); },
++ fetch: (u, init = {}) => new Promise((resolve, reject) => {
++ const req = { url: String(u), method: init.method || 'GET', signal: init.signal, resolve };
++ requests.push(req);
++ if (init.signal) init.signal.addEventListener('abort', () => reject(Object.assign(new Error('aborted'), { name: 'AbortError' })));
++ const json = (body) => resolve({ ok: true, status: 200, json: async () => body });
++ if (req.method === 'POST' && /\/webhook\/bins$/.test(req.url)) { bins.push(req); req.answer = () => json({ ok: true, binId: `bin${bins.length}` }); return; }
++ if (/\/webhook\/bins\/bin\d+$/.test(req.url)) {
++ return json({ ok: true, requests: [{ method: '<img src=x onerror=alert(1)>', headers: { 'x-evil<b>': '<script>alert(2)</script>' }, body: '<i>body</i>', timestamp: 0 }] });
++ }
++ reject(new Error(`unexpected ${req.method} ${req.url}`));
++ }),
++ };
++ ctx.window = ctx;
++ vm.createContext(ctx);
++ const shared = path.dirname(require.resolve('openvibe-shared/package.json'));
++ vm.runInContext(fs.readFileSync(path.join(shared, 'web-runtime.js'), 'utf8'), ctx, { filename: 'web-runtime.js' });
++ vm.runInContext(fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'dev-engine.js'), 'utf8'), ctx, { filename: 'dev-engine.js' });
++ vm.runInContext(PAGE, ctx, { filename: 'dev.html' });
++ const intervals = () => [...timers.values()].filter((t) => t.kind === 'interval');
++ return { ctx, el, scripts, timers, intervals, requests, bins, eval: (code) => vm.runInContext(code, ctx) };
++}
++const settle = async () => { for (let i = 0; i < 10; i++) await new Promise((r) => setImmediate(r)); };
++
++(async () => {
++ // ── The webhook tool: one poller, the newest run's ──
++ const p = page('webhook.openvibe.tools');
++ const first = p.ctx.run();
++ await settle();
++ const second = p.ctx.run(); // before the first bin was created
++ await settle();
++ p.bins[1].answer();
++ await second;
++ await first; // aborted, quietly
++ assert.ok(p.bins[0].signal.aborted, 'the first run\'s request was aborted');
++ assert.match(p.el('out').innerHTML, /bin2\/in/, 'the newest run owns the output');
++ assert.ok(!/bin1/.test(p.el('out').innerHTML));
++ assert.strictEqual(p.intervals().length, 1, 'one poller');
++
++ const third = p.ctx.run(); // a third run: bin3, and bin2's poller stops
++ await settle();
++ assert.strictEqual(p.intervals().length, 0, 'bin2\'s poller stopped when the third run began');
++ p.bins[2].answer();
++ await third;
++ assert.strictEqual(p.intervals().length, 1, 'still one poller after running again');
++ p.intervals()[0].f();
++ await settle();
++ const shown = p.el('whr').innerHTML;
++ assert.match(p.requests[p.requests.length - 1].url, /bin3$/, 'the poller asks for the newest bin');
++ assert.ok(!/<img|<script|<b>/.test(shown), `a sender's markup is text: ${shown}`);
++ assert.match(shown, /&lt;img src=x onerror=alert\(1\)&gt;/);
++ assert.match(shown, /x-evil&lt;b&gt;: &lt;script&gt;alert\(2\)&lt;\/script&gt;/);
++ p.ctx.clr();
++ assert.strictEqual(p.intervals().length, 0, 'Clear stops the poller');
++ assert.deepStrictEqual(JSON.parse(JSON.stringify(p.eval('RT.leaks()'))), []);
++
++ // ── A minifier run twice at once: Terser's script is added once ──
++ const q = page('jsminify.openvibe.tools');
++ q.el('in1').value = 'function f ( a ) { return a + 1 ; }';
++ const a = q.ctx.run(); const b = q.ctx.run();
++ await settle();
++ const terserTags = q.scripts.filter((s) => /terser/.test(s.src));
++ assert.strictEqual(terserTags.length, 1, 'one <script> for Terser');
++ q.ctx.Terser = { minify: async (src) => ({ code: src.replace(/\s+/g, '') }) };
++ terserTags[0].onload();
++ await Promise.all([a, b]);
++ assert.match(q.el('out').innerHTML, /functionf\(a\)\{returna\+1;\}/);
++
++ console.log('dev.html runtime: all checks passed');
++})().catch((e) => { console.error(e); process.exit(1); });
ddiff --git a/apps/img/package-lock.json b/apps/img/package-lock.json
iindex 92815fc..1f111f6 100644
---- a/apps/img/package-lock.json
++++ b/apps/img/package-lock.json
@@@ -20,7 +20,7 @@
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
"sharp": "^0.33.5",
"to-ico": "^1.1.5",
"uuid": "^11.1.0"
@@@ -1776,9 +1776,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "1.22.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
-- "integrity": "sha512-FtqbR8h6YphOnJNqklJED3HyqvTwZJl58gyiZ3QgzmZtZcA16O2NMGq67O4p7grLojwaIG5DhXDMgmZcpScxJQ==",
++ "version": "1.23.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
++ "integrity": "sha512-9B3vXn5P7JjHWrp3cNh7ZynLOmCUgw9cuz6moksU3fyuIfy7uoepa2wJmQeSyLoPU8q/zKRJVeBVzPfdhr0z3g==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/apps/img/package.json b/apps/img/package.json
iindex 090d48f..81b5a0f 100644
---- a/apps/img/package.json
++++ b/apps/img/package.json
@@@ -22,7 +22,7 @@
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
"sharp": "^0.33.5",
"to-ico": "^1.1.5",
"uuid": "^11.1.0"
ddiff --git a/apps/maps/package-lock.json b/apps/maps/package-lock.json
iindex 7e245bd..df96769 100644
---- a/apps/maps/package-lock.json
++++ b/apps/maps/package-lock.json
@@@ -19,7 +19,7 @@
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"node-cache": "^5.1.2",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0"
}
},
"node_modules/accepts": {
@@@ -1383,9 +1383,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "1.22.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
-- "integrity": "sha512-FtqbR8h6YphOnJNqklJED3HyqvTwZJl58gyiZ3QgzmZtZcA16O2NMGq67O4p7grLojwaIG5DhXDMgmZcpScxJQ==",
++ "version": "1.23.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
++ "integrity": "sha512-9B3vXn5P7JjHWrp3cNh7ZynLOmCUgw9cuz6moksU3fyuIfy7uoepa2wJmQeSyLoPU8q/zKRJVeBVzPfdhr0z3g==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/apps/maps/package.json b/apps/maps/package.json
iindex 43de955..c6a0b8b 100644
---- a/apps/maps/package.json
++++ b/apps/maps/package.json
@@@ -12,7 +12,7 @@
"dependencies": {
"axios": "^1.7.0",
"better-sqlite3": "^11.0.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
"cheerio": "^1.0.0-rc.12",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
ddiff --git a/apps/text/package-lock.json b/apps/text/package-lock.json
iindex f075a09..05a22af 100644
---- a/apps/text/package-lock.json
++++ b/apps/text/package-lock.json
@@@ -16,7 +16,7 @@
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0"
}
},
"node_modules/accepts": {
@@@ -969,9 +969,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "1.22.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
-- "integrity": "sha512-FtqbR8h6YphOnJNqklJED3HyqvTwZJl58gyiZ3QgzmZtZcA16O2NMGq67O4p7grLojwaIG5DhXDMgmZcpScxJQ==",
++ "version": "1.23.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
++ "integrity": "sha512-9B3vXn5P7JjHWrp3cNh7ZynLOmCUgw9cuz6moksU3fyuIfy7uoepa2wJmQeSyLoPU8q/zKRJVeBVzPfdhr0z3g==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/apps/text/package.json b/apps/text/package.json
iindex ed96f62..43c879f 100644
---- a/apps/text/package.json
++++ b/apps/text/package.json
@@@ -17,7 +17,7 @@
"better-sqlite3": "^11.0.0",
"express-rate-limit": "^7.1.5",
"dotenv": "^16.3.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0"
},
"license": "MIT"
}
ddiff --git a/apps/yt/package-lock.json b/apps/yt/package-lock.json
iindex efc0ef4..6f4098a 100644
---- a/apps/yt/package-lock.json
++++ b/apps/yt/package-lock.json
@@@ -17,7 +17,7 @@
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
"uuid": "^11.1.0"
}
},
@@@ -800,9 +800,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "1.22.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
-- "integrity": "sha512-FtqbR8h6YphOnJNqklJED3HyqvTwZJl58gyiZ3QgzmZtZcA16O2NMGq67O4p7grLojwaIG5DhXDMgmZcpScxJQ==",
++ "version": "1.23.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
++ "integrity": "sha512-9B3vXn5P7JjHWrp3cNh7ZynLOmCUgw9cuz6moksU3fyuIfy7uoepa2wJmQeSyLoPU8q/zKRJVeBVzPfdhr0z3g==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/apps/yt/package.json b/apps/yt/package.json
iindex fb50780..a0f21ed 100644
---- a/apps/yt/package.json
++++ b/apps/yt/package.json
@@@ -18,7 +18,7 @@
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"better-sqlite3": "^11.0.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.22.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
"jsonwebtoken": "^9.0.2",
"uuid": "^11.1.0"
}