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.
Changes
@@@ -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"@@@ -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"}}@@@ -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"@@@ -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"@@@ -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"@@@ -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"}}@@@ -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"@@@ -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"}}@@@ -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')?'sP.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>nnew file mode 100644@@@ -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, /<img src=x onerror=alert\(1\)>/);++ assert.match(shown, /x-evil<b>: <script>alert\(2\)<\/script>/);++ 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); });@@@ -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"@@@ -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"@@@ -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"@@@ -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",@@@ -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"@@@ -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"}@@@ -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"@@@ -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"}
@@ -21,7 +21,7 @@
@@ -21,7 +21,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.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-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"
"uuid": "^11.1.0"
}
}
},
},
@@ -1184,9 +1184,9 @@
@@ -1184,9 +1184,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -23,7 +23,7 @@
@@ -23,7 +23,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.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-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"
"uuid": "^11.1.0"
}
}
}
}
@@ -20,7 +20,7 @@
@@ -20,7 +20,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.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-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",
"pdf-lib": "^1.17.1",
"sharp": "^0.33.5",
"sharp": "^0.33.5",
"uuid": "^11.1.0"
"uuid": "^11.1.0"
@@ -1263,9 +1263,9 @@
@@ -1263,9 +1263,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -22,7 +22,7 @@
@@ -22,7 +22,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.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-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",
"pdf-lib": "^1.17.1",
"sharp": "^0.33.5",
"sharp": "^0.33.5",
"uuid": "^11.1.0"
"uuid": "^11.1.0"
@@ -15,7 +15,7 @@
@@ -15,7 +15,7 @@
"express": "^4.18.2",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
"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": {
"node_modules/accepts": {
@@ -968,9 +968,9 @@
@@ -968,9 +968,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -17,6 +17,6 @@
@@ -17,6 +17,6 @@
"better-sqlite3": "^11.0.0",
"better-sqlite3": "^11.0.0",
"express-rate-limit": "^7.1.5",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
"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"
}
}
}
}
@@ -19,7 +19,7 @@
@@ -19,7 +19,7 @@
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.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-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": {
"node_modules/accepts": {
@@ -1061,9 +1061,9 @@
@@ -1061,9 +1061,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -21,6 +21,6 @@
@@ -21,6 +21,6 @@
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.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-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"
}
}
}
}
@@ -147,6 +147,7 @@ a{color:inherit;text-decoration:none}
@@ -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/navbar.js"></script>
<script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/theme-loader.js" defer></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 src="/js/dev-engine.js"></script>
<script>
<script>
'use strict';
'use strict';
@@ -262,10 +263,14 @@ function buildRel(){
@@ -262,10 +263,14 @@ function buildRel(){
}
}
// ═══════════════ PROCESSORS ═══════════════
// ═══════════════ 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(){
async function run(){
+ const g=RT.nextRoute(),sc=RT.scope();
const i1=$('in1')?.value||'',i2=$('in2')?.value||'',o=$('out');
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={};
const P={};
@@ -312,7 +317,7 @@ P.cron=(s)=>DevEngine.cron(s);
@@ -312,7 +317,7 @@ P.cron=(s)=>DevEngine.cron(s);
P.beautify=(s,m)=>DevEngine.beautify(s,m);
P.beautify=(s,m)=>DevEngine.beautify(s,m);
// ── Single-purpose tools ─────────────────────────────────────
// ── 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 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 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>'});
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
@@ -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.curlconvert=(s,m)=>DevEngine.curlconvert(s,m);
-P.webhook=async()=>{
+P.webhook=async(s,m,s2,sc)=>{
const o=$('out');
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';
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>';
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)=>{
P.color=(s)=>{
@@ -420,7 +426,7 @@ async function saveAsPaste(btn){
@@ -420,7 +426,7 @@ async function saveAsPaste(btn){
toast('Saved as a paste');
toast('Saved as a paste');
}catch(e){toast(e.message)}finally{btn.disabled=false}
}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 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)}
function toast(msg){const t=$('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2000)}
</script>
</script>
new file mode 100644
new file mode 100644
@@ -0,0 +1,115 @@
@@ -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, /<img src=x onerror=alert\(1\)>/);
+ assert.match(shown, /x-evil<b>: <script>alert\(2\)<\/script>/);
+ 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); });
@@ -20,7 +20,7 @@
@@ -20,7 +20,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.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-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",
"sharp": "^0.33.5",
"to-ico": "^1.1.5",
"to-ico": "^1.1.5",
"uuid": "^11.1.0"
"uuid": "^11.1.0"
@@ -1776,9 +1776,9 @@
@@ -1776,9 +1776,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -22,7 +22,7 @@
@@ -22,7 +22,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.63.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-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",
"sharp": "^0.33.5",
"to-ico": "^1.1.5",
"to-ico": "^1.1.5",
"uuid": "^11.1.0"
"uuid": "^11.1.0"
@@ -19,7 +19,7 @@
@@ -19,7 +19,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"node-cache": "^5.1.2",
"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": {
"node_modules/accepts": {
@@ -1383,9 +1383,9 @@
@@ -1383,9 +1383,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -12,7 +12,7 @@
@@ -12,7 +12,7 @@
"dependencies": {
"dependencies": {
"axios": "^1.7.0",
"axios": "^1.7.0",
"better-sqlite3": "^11.0.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",
"cheerio": "^1.0.0-rc.12",
"cookie-parser": "^1.4.7",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"cors": "^2.8.5",
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"express": "^4.18.2",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
"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": {
"node_modules/accepts": {
@@ -969,9 +969,9 @@
@@ -969,9 +969,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"better-sqlite3": "^11.0.0",
"better-sqlite3": "^11.0.0",
"express-rate-limit": "^7.1.5",
"express-rate-limit": "^7.1.5",
"dotenv": "^16.3.1",
"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"
"license": "MIT"
}
}
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2",
"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"
"uuid": "^11.1.0"
}
}
},
},
@@ -800,9 +800,9 @@
@@ -800,9 +800,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -18,7 +18,7 @@
@@ -18,7 +18,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"better-sqlite3": "^11.0.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",
"jsonwebtoken": "^9.0.2",
"uuid": "^11.1.0"
"uuid": "^11.1.0"
}
}