openvibers/openvibe.sdk Commits
The OpenVibe Frame: openvibe-sdk/chrome is now openvibe-sdk/frame (mountFrame, frameTags, frameConfig) — the navbar, footer and themes every OpenVibe site sits in; 'chrome' was borrowed UI jargon easily mistaken for the browser. Not yet tagged, so no alias
Changes
@@@ -5,7 +5,7 @@ release may change an API and says so here.## 0.8.0 (2026-09-24)--`openvibe-sdk/chrome`: the OpenVibe network's shared chrome for any app, first-party or not. It covers:++`openvibe-sdk/frame`: **the OpenVibe Frame**, the navbar, footer and themes every OpenVibe site sits in, for any app, first-party or not. It covers:- the universal navbar: one account menu, notifications, the site switcher, and sign in and out through your own session endpoints (`sessionUrl`, `loginUrl`, `logoutUrl` with `{path}`/`{url}`);- the shared footer, with network and legal links, the "shipped X ago" line and an Updates link;@@@ -13,7 +13,7 @@ release may change an API and says so here.These are the same files every OpenVibe site runs, served by OpenVibe.Network from the published openvibe-shared release, so nothing is bundled into your app.--`mountChrome(opts)` loads and mounts them in the browser. It never throws for a network failure; a part that cannot load resolves to `null`. `chromeTags(opts)` returns the same markup as strings for server-rendered pages, with the configuration escaped for its `<script>`. `chromeConfig` and `scriptUrls` are exported for custom setups. The base must be an https origin (or `http://localhost` in development). Browser-safe; no dependencies.++`mountFrame(opts)` loads and mounts them in the browser. It never throws for a network failure; a part that cannot load resolves to `null`. `frameTags(opts)` returns the same markup as strings for server-rendered pages, with the configuration escaped for its `<script>`. `frameConfig` and `scriptUrls` are exported for custom setups. The base must be an https origin (or `http://localhost` in development). Browser-safe; no dependencies.## 0.7.0 (2026-09-24)@@@ -237,13 +237,13 @@ app.post('/internal/events', express.raw({ type: 'application/json' }), (req, reTurn `requireV2` on once Events sends v2 to you: a replayed v1-only delivery then fails. The lower-level checks are `verifyDeliveryV2(raw, headers, secret, { toleranceSec, now })` (constant-time; also refuses a `X-OpenVibe-Timestamp` that differs from `t`) and `verifyDelivery(raw, signatureHeader, secret)` (v1, unchanged). Keep your clock in sync (NTP): the window is ±300 s both ways. In tests, `signDeliveryHeaders(raw, secret, { now })` returns the three headers as Events sends them.--### The shared chrome++### The OpenVibe FrameEvery OpenVibe site uses the same navbar and footer. Your app can use them too, so people move between it and the network without a jump:```js--// Browser (CommonJS or ESM: openvibe-sdk/chrome)--const { navbar } = await mountChrome({++// Browser (CommonJS or ESM: openvibe-sdk/frame)++const { navbar } = await mountFrame({service: 'myapp',brand: { name: 'My App' },links: [{ label: 'Home', href: '/' }, { label: 'Docs', href: '/docs' }],@@@ -255,9 +255,9 @@ const { navbar } = await mountChrome({});```--For server-rendered pages, `chromeTags(opts)` returns `{ head, bodyStart, bodyEnd }`: put `head` in `<head>`, `bodyStart` first in `<body>` and `bodyEnd` last. Your CSP needs `script-src https://openvibe.network` and `connect-src https://openvibe.network`.++For server-rendered pages, `frameTags(opts)` returns `{ head, bodyStart, bodyEnd }`: put `head` in `<head>`, `bodyStart` first in `<body>` and `bodyEnd` last. Your CSP needs `script-src https://openvibe.network` and `connect-src https://openvibe.network`.--The chrome is progressive: if openvibe.network cannot be reached, your page still renders, and the part that failed resolves to `null`. Signed-in state comes from the shared `ov_token`, and otherwise from your `sessionUrl`. To have your users recognised across the network, sign them in with OpenVibe (`openvibe-sdk/auth`).++The Frame is progressive: if openvibe.network cannot be reached, your page still renders, and the part that failed resolves to `null`. Signed-in state comes from the shared `ov_token`, and otherwise from your `sessionUrl`. To have your users recognised across the network, sign them in with OpenVibe (`openvibe-sdk/auth`).### Toolsddeleted file mode 100644@@@ -1,7 +0,0 @@--// Generated by scripts/esm.js from src/chrome.js. Do not edit.--import cjs from '../src/chrome.js';----export const {-- mountChrome, chromeTags, chromeConfig, scriptUrls, DEFAULT_BASE,--} = cjs;--export default cjs;nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/chrome.js. Do not edit.++import cjs from '../src/chrome.js';++++export const {++ mountChrome, chromeTags, chromeConfig, scriptUrls, DEFAULT_BASE,++} = cjs;++export default cjs;@@@ -80,10 +80,10 @@"import": "./esm/projects.mjs","default": "./src/projects.js"},-- "./chrome": {-- "types": "./types/chrome.d.ts",-- "import": "./esm/chrome.mjs",-- "default": "./src/chrome.js"++ "./frame": {++ "types": "./types/frame.d.ts",++ "import": "./esm/frame.mjs",++ "default": "./src/frame.js"},"./vip": {"types": "./types/vip.d.ts",@@@ -27,7 +27,7 @@ const ENTRIES = {'tools.mjs': '../src/tools.js','projects.mjs': '../src/projects.js','vip.mjs': '../src/vip.js',-- 'chrome.mjs': '../src/chrome.js',++ 'frame.mjs': '../src/frame.js','testing.mjs': '../src/testing/index.js',};ddeleted file mode 100644@@@ -1,150 +0,0 @@--'use strict';----/**-- * openvibe-sdk/chrome — the OpenVibe network's shared chrome for any app: the universal navbar (one-- * account menu, notifications, the site switcher, sign in and out), the shared footer (network-- * links, legal links, the "shipped X ago" line and an Updates link), the "shipped" views and the-- * theme loader. These are the same files every OpenVibe site runs, served by OpenVibe.Network from-- * the published openvibe-shared release. No dependencies; nothing is bundled into your app.-- *-- * In the browser:-- *-- * // mountChrome from the openvibe-sdk/chrome entry (CommonJS or ESM)-- * const { navbar, footer } = await mountChrome({-- * service: 'myapp', // your app's id (brand + analytics)-- * brand: { name: 'My App' }, // optional brand override-- * links: [{ label: 'Home', href: '/' }, { label: 'Docs', href: '/docs' }],-- * menu: { before: [{ label: 'My projects', href: '/projects', icon: 'fa-folder' }] },-- * sessionUrl: '/auth/me', // your server session ({ user }), optional-- * loginUrl: '/auth/login?next={path}', // {url} full return URL, {path} local path-- * logoutUrl: '/auth/logout?next={path}', // Sign out ends your session too-- * footer: { links: [{ heading: 'My App', items: [{ name: 'About', url: '/about' }] }], updates: '/updates' },-- * });-- *-- * Server-rendered apps put chromeTags(opts) in <head>/<body> instead (the same markup this module-- * would inject, as a string; the JSON config is escaped for a <script> element):-- *-- * // chromeTags from the openvibe-sdk/chrome entry-- * const t = chromeTags({ service: 'myapp', links: [...] });-- * html = `<head>${t.head}</head><body>${t.bodyStart}…${t.bodyEnd}</body>`;-- *-- * Your CSP must allow `script-src https://openvibe.network` and `connect-src https://openvibe.network`-- * (the navbar asks /api/auth/me and the notifications there; the shipped line reads-- * /api/v1/changelog). Signed-in state comes from the shared ov_token, else from sessionUrl. For-- * your own users to be recognised across the network, sign them in with OpenVibe (openvibe-sdk/auth).-- * The chrome is progressive: if openvibe.network is unreachable your page still renders.-- */----const DEFAULT_BASE = 'https://openvibe.network';--const FILES = { theme: 'theme-loader.js', navbar: 'navbar.js', footer: 'footer.js', shipped: 'shipped.js' };----function baseOf(base) {-- const b = String(base || DEFAULT_BASE).replace(/\/+$/, '');-- const ok = /^https:\/\/[a-z0-9.-]+(:\d+)?$/i.test(b) || /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(b);-- if (!ok) throw new TypeError(`openvibe-sdk/chrome: base must be an https origin (or http://localhost for development), got ${base}`);-- return b;--}----/** The script URLs on a base: the release OpenVibe.Network currently serves at /shared/. */--function scriptUrls({ base } = {}) {-- const b = baseOf(base);-- const out = {};-- for (const [k, f] of Object.entries(FILES)) out[k] = `${b}/shared/${f}`;-- return out;--}----/** The navbar and footer configurations mountChrome passes to OpenVibeNavbar.init / OpenVibeFooter.init. */--function chromeConfig(opts = {}) {-- const o = opts || {};-- const b = baseOf(o.base);-- const navbar = {-- service: o.service || 'app',-- apiBase: b,-- ...(o.brand ? { brand: o.brand } : {}),-- ...(Array.isArray(o.links) ? { links: o.links } : {}),-- ...(o.menu ? { menu: o.menu } : {}),-- ...(o.sessionUrl ? { sessionUrl: o.sessionUrl } : {}),-- ...(o.loginUrl ? { loginUrl: o.loginUrl } : {}),-- ...(o.logoutUrl ? { logoutUrl: o.logoutUrl } : {}),-- ...(o.silentLogin ? { silentLogin: o.silentLogin } : {}),-- ...(o.navbar && typeof o.navbar === 'object' ? o.navbar : {}),-- };-- const footer = o.footer === false ? null : {-- service: o.service || 'app',-- mount: '#ov-footer',-- variant: 'full',-- apiBase: b,-- ...(o.brand && o.brand.name ? { brandName: o.brand.name } : {}),-- ...(o.shipped === false ? { shipped: false } : {}),-- ...(o.footer && typeof o.footer === 'object' ? o.footer : {}),-- };-- return { navbar, footer };--}----const jsonForScript = (v) => JSON.stringify(v).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');--const attr = (s) => String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');----/** Markup for server-rendered pages: { head, bodyStart, bodyEnd } strings. */--function chromeTags(opts = {}) {-- const urls = scriptUrls(opts);-- const cfg = chromeConfig(opts);-- const head = [-- opts.theme === false ? '' : `<script src="${attr(urls.theme)}" defer></script>`,-- `<script src="${attr(urls.navbar)}" defer></script>`,-- cfg.footer ? `<script src="${attr(urls.footer)}" defer></script>` : '',-- ].filter(Boolean).join('\n');-- const bodyStart = '<div id="navbar-mount"></div>';-- const init = `document.addEventListener('DOMContentLoaded',function(){var c=${jsonForScript(cfg)};`-- + `try{if(window.OpenVibeNavbar)OpenVibeNavbar.init(c.navbar);}catch(e){}`-- + `try{if(c.footer&&window.OpenVibeFooter)OpenVibeFooter.init(c.footer);}catch(e){}});`;-- const bodyEnd = `${cfg.footer ? '<footer id="ov-footer"></footer>\n' : ''}<script>${init}</script>`;-- return { head, bodyStart, bodyEnd, config: cfg, urls };--}----function loadScript(doc, src) {-- return new Promise((resolve) => {-- const existing = doc.querySelector(`script[data-ov-chrome="${src}"]`);-- if (existing) { if (existing.dataset.loaded) resolve(true); else existing.addEventListener('load', () => resolve(true)); return; }-- const s = doc.createElement('script');-- s.src = src;-- s.async = true;-- s.setAttribute('data-ov-chrome', src);-- s.addEventListener('load', () => { s.dataset.loaded = '1'; resolve(true); });-- s.addEventListener('error', () => resolve(false));-- (doc.head || doc.documentElement).appendChild(s);-- });--}----/**-- * Browser: load the shared chrome and mount it. Resolves { navbar, footer } (the initialised-- * globals, or null for a part that could not load). Never throws for a network failure.-- */--async function mountChrome(opts = {}) {-- const doc = (opts && opts.document) || (typeof document !== 'undefined' ? document : null);-- const win = (opts && opts.window) || (typeof window !== 'undefined' ? window : null);-- if (!doc || !win) throw new Error('openvibe-sdk/chrome: mountChrome runs in a browser; use chromeTags() on the server');-- const urls = scriptUrls(opts);-- const cfg = chromeConfig(opts);-- if (!doc.getElementById('navbar-mount')) {-- const m = doc.createElement('div');-- m.id = 'navbar-mount';-- doc.body.insertBefore(m, doc.body.firstChild);-- }-- if (cfg.footer && !doc.querySelector(cfg.footer.mount)) {-- const f = doc.createElement('footer');-- f.id = String(cfg.footer.mount).replace(/^#/, '');-- doc.body.appendChild(f);-- }-- const loads = [];-- if (opts.theme !== false) loads.push(loadScript(doc, urls.theme));-- const navOk = loadScript(doc, urls.navbar);-- const footOk = cfg.footer ? loadScript(doc, urls.footer) : Promise.resolve(false);-- await Promise.all([...loads, navOk, footOk]);-- let navbar = null;-- let footer = null;-- try { if (win.OpenVibeNavbar) { win.OpenVibeNavbar.init(cfg.navbar); navbar = win.OpenVibeNavbar; } } catch { navbar = null; }-- try { if (cfg.footer && win.OpenVibeFooter) { win.OpenVibeFooter.init(cfg.footer); footer = win.OpenVibeFooter; } } catch { footer = null; }-- return { navbar, footer };--}----module.exports = { mountChrome, chromeTags, chromeConfig, scriptUrls, DEFAULT_BASE };nnew file mode 100644@@@ -0,0 +1,150 @@++'use strict';++++/**++ * openvibe-sdk/chrome — the OpenVibe network's shared chrome for any app: the universal navbar (one++ * account menu, notifications, the site switcher, sign in and out), the shared footer (network++ * links, legal links, the "shipped X ago" line and an Updates link), the "shipped" views and the++ * theme loader. These are the same files every OpenVibe site runs, served by OpenVibe.Network from++ * the published openvibe-shared release. No dependencies; nothing is bundled into your app.++ *++ * In the browser:++ *++ * // mountChrome from the openvibe-sdk/chrome entry (CommonJS or ESM)++ * const { navbar, footer } = await mountChrome({++ * service: 'myapp', // your app's id (brand + analytics)++ * brand: { name: 'My App' }, // optional brand override++ * links: [{ label: 'Home', href: '/' }, { label: 'Docs', href: '/docs' }],++ * menu: { before: [{ label: 'My projects', href: '/projects', icon: 'fa-folder' }] },++ * sessionUrl: '/auth/me', // your server session ({ user }), optional++ * loginUrl: '/auth/login?next={path}', // {url} full return URL, {path} local path++ * logoutUrl: '/auth/logout?next={path}', // Sign out ends your session too++ * footer: { links: [{ heading: 'My App', items: [{ name: 'About', url: '/about' }] }], updates: '/updates' },++ * });++ *++ * Server-rendered apps put chromeTags(opts) in <head>/<body> instead (the same markup this module++ * would inject, as a string; the JSON config is escaped for a <script> element):++ *++ * // chromeTags from the openvibe-sdk/chrome entry++ * const t = chromeTags({ service: 'myapp', links: [...] });++ * html = `<head>${t.head}</head><body>${t.bodyStart}…${t.bodyEnd}</body>`;++ *++ * Your CSP must allow `script-src https://openvibe.network` and `connect-src https://openvibe.network`++ * (the navbar asks /api/auth/me and the notifications there; the shipped line reads++ * /api/v1/changelog). Signed-in state comes from the shared ov_token, else from sessionUrl. For++ * your own users to be recognised across the network, sign them in with OpenVibe (openvibe-sdk/auth).++ * The chrome is progressive: if openvibe.network is unreachable your page still renders.++ */++++const DEFAULT_BASE = 'https://openvibe.network';++const FILES = { theme: 'theme-loader.js', navbar: 'navbar.js', footer: 'footer.js', shipped: 'shipped.js' };++++function baseOf(base) {++ const b = String(base || DEFAULT_BASE).replace(/\/+$/, '');++ const ok = /^https:\/\/[a-z0-9.-]+(:\d+)?$/i.test(b) || /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(b);++ if (!ok) throw new TypeError(`openvibe-sdk/chrome: base must be an https origin (or http://localhost for development), got ${base}`);++ return b;++}++++/** The script URLs on a base: the release OpenVibe.Network currently serves at /shared/. */++function scriptUrls({ base } = {}) {++ const b = baseOf(base);++ const out = {};++ for (const [k, f] of Object.entries(FILES)) out[k] = `${b}/shared/${f}`;++ return out;++}++++/** The navbar and footer configurations mountChrome passes to OpenVibeNavbar.init / OpenVibeFooter.init. */++function chromeConfig(opts = {}) {++ const o = opts || {};++ const b = baseOf(o.base);++ const navbar = {++ service: o.service || 'app',++ apiBase: b,++ ...(o.brand ? { brand: o.brand } : {}),++ ...(Array.isArray(o.links) ? { links: o.links } : {}),++ ...(o.menu ? { menu: o.menu } : {}),++ ...(o.sessionUrl ? { sessionUrl: o.sessionUrl } : {}),++ ...(o.loginUrl ? { loginUrl: o.loginUrl } : {}),++ ...(o.logoutUrl ? { logoutUrl: o.logoutUrl } : {}),++ ...(o.silentLogin ? { silentLogin: o.silentLogin } : {}),++ ...(o.navbar && typeof o.navbar === 'object' ? o.navbar : {}),++ };++ const footer = o.footer === false ? null : {++ service: o.service || 'app',++ mount: '#ov-footer',++ variant: 'full',++ apiBase: b,++ ...(o.brand && o.brand.name ? { brandName: o.brand.name } : {}),++ ...(o.shipped === false ? { shipped: false } : {}),++ ...(o.footer && typeof o.footer === 'object' ? o.footer : {}),++ };++ return { navbar, footer };++}++++const jsonForScript = (v) => JSON.stringify(v).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');++const attr = (s) => String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');++++/** Markup for server-rendered pages: { head, bodyStart, bodyEnd } strings. */++function chromeTags(opts = {}) {++ const urls = scriptUrls(opts);++ const cfg = chromeConfig(opts);++ const head = [++ opts.theme === false ? '' : `<script src="${attr(urls.theme)}" defer></script>`,++ `<script src="${attr(urls.navbar)}" defer></script>`,++ cfg.footer ? `<script src="${attr(urls.footer)}" defer></script>` : '',++ ].filter(Boolean).join('\n');++ const bodyStart = '<div id="navbar-mount"></div>';++ const init = `document.addEventListener('DOMContentLoaded',function(){var c=${jsonForScript(cfg)};`++ + `try{if(window.OpenVibeNavbar)OpenVibeNavbar.init(c.navbar);}catch(e){}`++ + `try{if(c.footer&&window.OpenVibeFooter)OpenVibeFooter.init(c.footer);}catch(e){}});`;++ const bodyEnd = `${cfg.footer ? '<footer id="ov-footer"></footer>\n' : ''}<script>${init}</script>`;++ return { head, bodyStart, bodyEnd, config: cfg, urls };++}++++function loadScript(doc, src) {++ return new Promise((resolve) => {++ const existing = doc.querySelector(`script[data-ov-chrome="${src}"]`);++ if (existing) { if (existing.dataset.loaded) resolve(true); else existing.addEventListener('load', () => resolve(true)); return; }++ const s = doc.createElement('script');++ s.src = src;++ s.async = true;++ s.setAttribute('data-ov-chrome', src);++ s.addEventListener('load', () => { s.dataset.loaded = '1'; resolve(true); });++ s.addEventListener('error', () => resolve(false));++ (doc.head || doc.documentElement).appendChild(s);++ });++}++++/**++ * Browser: load the shared chrome and mount it. Resolves { navbar, footer } (the initialised++ * globals, or null for a part that could not load). Never throws for a network failure.++ */++async function mountChrome(opts = {}) {++ const doc = (opts && opts.document) || (typeof document !== 'undefined' ? document : null);++ const win = (opts && opts.window) || (typeof window !== 'undefined' ? window : null);++ if (!doc || !win) throw new Error('openvibe-sdk/chrome: mountChrome runs in a browser; use chromeTags() on the server');++ const urls = scriptUrls(opts);++ const cfg = chromeConfig(opts);++ if (!doc.getElementById('navbar-mount')) {++ const m = doc.createElement('div');++ m.id = 'navbar-mount';++ doc.body.insertBefore(m, doc.body.firstChild);++ }++ if (cfg.footer && !doc.querySelector(cfg.footer.mount)) {++ const f = doc.createElement('footer');++ f.id = String(cfg.footer.mount).replace(/^#/, '');++ doc.body.appendChild(f);++ }++ const loads = [];++ if (opts.theme !== false) loads.push(loadScript(doc, urls.theme));++ const navOk = loadScript(doc, urls.navbar);++ const footOk = cfg.footer ? loadScript(doc, urls.footer) : Promise.resolve(false);++ await Promise.all([...loads, navOk, footOk]);++ let navbar = null;++ let footer = null;++ try { if (win.OpenVibeNavbar) { win.OpenVibeNavbar.init(cfg.navbar); navbar = win.OpenVibeNavbar; } } catch { navbar = null; }++ try { if (cfg.footer && win.OpenVibeFooter) { win.OpenVibeFooter.init(cfg.footer); footer = win.OpenVibeFooter; } } catch { footer = null; }++ return { navbar, footer };++}++++module.exports = { mountChrome, chromeTags, chromeConfig, scriptUrls, DEFAULT_BASE };ddeleted file mode 100644@@@ -1,84 +0,0 @@--'use strict';--// openvibe-sdk/chrome: any app mounts the network's shared navbar, footer, shipped line and themes,--// with its own session endpoints; server-rendered apps get the same markup as strings.--const assert = require('node:assert/strict');--const { mountChrome, chromeTags, chromeConfig, scriptUrls } = require('../src/chrome');----const opts = {-- service: 'myapp',-- brand: { name: 'My App' },-- links: [{ label: 'Docs </script><script>alert(1)</script>', href: '/docs' }],-- menu: { before: [{ label: 'My projects', href: '/projects' }] },-- sessionUrl: '/auth/me', loginUrl: '/auth/login?next={path}', logoutUrl: '/auth/logout?next={path}',-- footer: { updates: '/updates' },--};----// Config mapping--const cfg = chromeConfig(opts);--assert.equal(cfg.navbar.service, 'myapp');--assert.equal(cfg.navbar.apiBase, 'https://openvibe.network');--assert.equal(cfg.navbar.logoutUrl, '/auth/logout?next={path}');--assert.deepEqual(cfg.navbar.menu.before[0], { label: 'My projects', href: '/projects' });--assert.equal(cfg.footer.updates, '/updates');--assert.equal(cfg.footer.brandName, 'My App');--assert.equal(chromeConfig({ footer: false }).footer, null);--assert.equal(chromeConfig({ shipped: false }).footer.shipped, false);----// URLs and base validation--assert.equal(scriptUrls().navbar, 'https://openvibe.network/shared/navbar.js');--assert.equal(scriptUrls({ base: 'http://localhost:4000/' }).footer, 'http://localhost:4000/shared/footer.js');--assert.throws(() => scriptUrls({ base: 'http://evil.example' }), /https origin/);--assert.throws(() => scriptUrls({ base: 'javascript:alert(1)' }), /https origin/);----// SSR tags: the config cannot break out of its <script>--const t = chromeTags(opts);--assert.ok(t.head.includes('https://openvibe.network/shared/navbar.js') && t.head.includes('theme-loader.js') && t.head.includes('footer.js'));--assert.equal(t.bodyStart, '<div id="navbar-mount"></div>');--assert.ok(t.bodyEnd.includes('<footer id="ov-footer"></footer>'));--assert.ok(!/<\/script><script>alert/.test(t.bodyEnd), 'a label cannot close the init script');--assert.equal((t.bodyEnd.match(/<\/script>/g) || []).length, 1);--assert.ok(!chromeTags({ theme: false }).head.includes('theme-loader'));----// Browser mount with a minimal DOM: scripts "load", globals appear, init receives the config.--function fakeDom() {-- const inits = {};-- const win = {};-- const nodes = [];-- const el = (tag) => {-- const n = { tagName: tag.toUpperCase(), id: '', dataset: {}, attrs: {}, listeners: {}, children: [],-- setAttribute(k, v) { this.attrs[k] = v; }, addEventListener(ev, fn) { (this.listeners[ev] = this.listeners[ev] || []).push(fn); },-- appendChild(c) { this.children.push(c); nodes.push(c); if (c.tagName === 'SCRIPT') setImmediate(() => {-- if (c.src.endsWith('navbar.js')) win.OpenVibeNavbar = { init: (x) => { inits.navbar = x; } };-- if (c.src.endsWith('footer.js')) win.OpenVibeFooter = { init: (x) => { inits.footer = x; } };-- (c.listeners.load || []).forEach((f) => f());-- }); return c; },-- insertBefore(c) { nodes.push(c); return c; } };-- return n;-- };-- const doc = {-- head: el('head'), body: el('body'), documentElement: el('html'),-- createElement: el,-- getElementById: (id) => nodes.find((n) => n.id === id) || null,-- querySelector: (sel) => sel.startsWith('#') ? nodes.find((n) => n.id === sel.slice(1)) || null : nodes.find((n) => n.tagName === 'SCRIPT' && sel.includes(n.src)) || null,-- };-- doc.body.firstChild = null;-- return { doc, win, inits, nodes };--}----(async () => {-- const d = fakeDom();-- const r = await mountChrome({ ...opts, document: d.doc, window: d.win });-- assert.ok(r.navbar && r.footer);-- assert.equal(d.inits.navbar.sessionUrl, '/auth/me');-- assert.equal(d.inits.footer.mount, '#ov-footer');-- assert.ok(d.doc.getElementById('navbar-mount') && d.doc.getElementById('ov-footer'), 'mount points created');-- assert.deepEqual(d.nodes.filter((n) => n.tagName === 'SCRIPT').map((n) => n.src.split('/').pop()).sort(), ['footer.js', 'navbar.js', 'theme-loader.js']);---- // A failed load leaves that part null and never throws.-- const d2 = fakeDom();-- d2.doc.head.appendChild = function (c) { d2.nodes.push(c); setImmediate(() => (c.listeners.error || []).forEach((f) => f())); return c; };-- const r2 = await mountChrome({ document: d2.doc, window: d2.win });-- assert.deepEqual(r2, { navbar: null, footer: null });-- await assert.rejects(() => mountChrome({ document: null, window: null }), /browser/);-- console.log('chrome: all checks passed');--})().catch((e) => { console.error(e); process.exit(1); });nnew file mode 100644@@@ -0,0 +1,84 @@++'use strict';++// openvibe-sdk/chrome: any app mounts the network's shared navbar, footer, shipped line and themes,++// with its own session endpoints; server-rendered apps get the same markup as strings.++const assert = require('node:assert/strict');++const { mountChrome, chromeTags, chromeConfig, scriptUrls } = require('../src/chrome');++++const opts = {++ service: 'myapp',++ brand: { name: 'My App' },++ links: [{ label: 'Docs </script><script>alert(1)</script>', href: '/docs' }],++ menu: { before: [{ label: 'My projects', href: '/projects' }] },++ sessionUrl: '/auth/me', loginUrl: '/auth/login?next={path}', logoutUrl: '/auth/logout?next={path}',++ footer: { updates: '/updates' },++};++++// Config mapping++const cfg = chromeConfig(opts);++assert.equal(cfg.navbar.service, 'myapp');++assert.equal(cfg.navbar.apiBase, 'https://openvibe.network');++assert.equal(cfg.navbar.logoutUrl, '/auth/logout?next={path}');++assert.deepEqual(cfg.navbar.menu.before[0], { label: 'My projects', href: '/projects' });++assert.equal(cfg.footer.updates, '/updates');++assert.equal(cfg.footer.brandName, 'My App');++assert.equal(chromeConfig({ footer: false }).footer, null);++assert.equal(chromeConfig({ shipped: false }).footer.shipped, false);++++// URLs and base validation++assert.equal(scriptUrls().navbar, 'https://openvibe.network/shared/navbar.js');++assert.equal(scriptUrls({ base: 'http://localhost:4000/' }).footer, 'http://localhost:4000/shared/footer.js');++assert.throws(() => scriptUrls({ base: 'http://evil.example' }), /https origin/);++assert.throws(() => scriptUrls({ base: 'javascript:alert(1)' }), /https origin/);++++// SSR tags: the config cannot break out of its <script>++const t = chromeTags(opts);++assert.ok(t.head.includes('https://openvibe.network/shared/navbar.js') && t.head.includes('theme-loader.js') && t.head.includes('footer.js'));++assert.equal(t.bodyStart, '<div id="navbar-mount"></div>');++assert.ok(t.bodyEnd.includes('<footer id="ov-footer"></footer>'));++assert.ok(!/<\/script><script>alert/.test(t.bodyEnd), 'a label cannot close the init script');++assert.equal((t.bodyEnd.match(/<\/script>/g) || []).length, 1);++assert.ok(!chromeTags({ theme: false }).head.includes('theme-loader'));++++// Browser mount with a minimal DOM: scripts "load", globals appear, init receives the config.++function fakeDom() {++ const inits = {};++ const win = {};++ const nodes = [];++ const el = (tag) => {++ const n = { tagName: tag.toUpperCase(), id: '', dataset: {}, attrs: {}, listeners: {}, children: [],++ setAttribute(k, v) { this.attrs[k] = v; }, addEventListener(ev, fn) { (this.listeners[ev] = this.listeners[ev] || []).push(fn); },++ appendChild(c) { this.children.push(c); nodes.push(c); if (c.tagName === 'SCRIPT') setImmediate(() => {++ if (c.src.endsWith('navbar.js')) win.OpenVibeNavbar = { init: (x) => { inits.navbar = x; } };++ if (c.src.endsWith('footer.js')) win.OpenVibeFooter = { init: (x) => { inits.footer = x; } };++ (c.listeners.load || []).forEach((f) => f());++ }); return c; },++ insertBefore(c) { nodes.push(c); return c; } };++ return n;++ };++ const doc = {++ head: el('head'), body: el('body'), documentElement: el('html'),++ createElement: el,++ getElementById: (id) => nodes.find((n) => n.id === id) || null,++ querySelector: (sel) => sel.startsWith('#') ? nodes.find((n) => n.id === sel.slice(1)) || null : nodes.find((n) => n.tagName === 'SCRIPT' && sel.includes(n.src)) || null,++ };++ doc.body.firstChild = null;++ return { doc, win, inits, nodes };++}++++(async () => {++ const d = fakeDom();++ const r = await mountChrome({ ...opts, document: d.doc, window: d.win });++ assert.ok(r.navbar && r.footer);++ assert.equal(d.inits.navbar.sessionUrl, '/auth/me');++ assert.equal(d.inits.footer.mount, '#ov-footer');++ assert.ok(d.doc.getElementById('navbar-mount') && d.doc.getElementById('ov-footer'), 'mount points created');++ assert.deepEqual(d.nodes.filter((n) => n.tagName === 'SCRIPT').map((n) => n.src.split('/').pop()).sort(), ['footer.js', 'navbar.js', 'theme-loader.js']);++++ // A failed load leaves that part null and never throws.++ const d2 = fakeDom();++ d2.doc.head.appendChild = function (c) { d2.nodes.push(c); setImmediate(() => (c.listeners.error || []).forEach((f) => f())); return c; };++ const r2 = await mountChrome({ document: d2.doc, window: d2.win });++ assert.deepEqual(r2, { navbar: null, footer: null });++ await assert.rejects(() => mountChrome({ document: null, window: null }), /browser/);++ console.log('chrome: all checks passed');++})().catch((e) => { console.error(e); process.exit(1); });@@@ -22,7 +22,7 @@ run([const out = execFileSync(npm, ['pack', '--json', '--pack-destination', tmp], { cwd: ROOT, env, encoding: 'utf8' });const info = JSON.parse(out)[0];const files = info.files.map((f) => f.path);-- for (const must of ['package.json', 'index.js', 'browser.js', 'LICENSE', 'README.md', 'CHANGELOG.md', 'src/core/client.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'src/vip.js', 'esm/vip.mjs', 'types/vip.d.ts', 'src/chrome.js', 'esm/chrome.mjs', 'types/chrome.d.ts', 'esm/core.mjs', 'esm/jobs.mjs', 'esm/tools.mjs', 'types/tools.d.ts', 'types/core.d.ts', 'types/contracts.d.ts', 'types/bundle.d.ts', 'browser/openvibe-sdk.mjs']) {++ for (const must of ['package.json', 'index.js', 'browser.js', 'LICENSE', 'README.md', 'CHANGELOG.md', 'src/core/client.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'src/vip.js', 'esm/vip.mjs', 'types/vip.d.ts', 'src/frame.js', 'esm/frame.mjs', 'types/frame.d.ts', 'esm/core.mjs', 'esm/jobs.mjs', 'esm/tools.mjs', 'types/tools.d.ts', 'types/core.d.ts', 'types/contracts.d.ts', 'types/bundle.d.ts', 'browser/openvibe-sdk.mjs']) {assert.ok(files.includes(must), `tarball contains ${must}`);}assert.ok(!files.some((f) => f.startsWith('test/') || f.startsWith('scripts/') || f.startsWith('.github/')), 'no tests or tooling in the tarball');ddeleted file mode 100644@@@ -1,46 +0,0 @@--// openvibe-sdk/chrome — the OpenVibe network's shared navbar, footer, "shipped" views and theme loader for any app.----export interface ChromeLink { label: string; href: string; icon?: string; active?: boolean }--export interface ChromeMenuItem { label: string; href?: string; icon?: string; onClick?: () => void; danger?: boolean; external?: boolean }--export interface ChromeFooterConfig {-- links?: Array<{ heading: string; items: Array<{ name: string; url: string }> }>;-- variant?: 'full' | 'compact';-- /** This app's update log; default: https://openvibe.network/updates?site=<host>. */-- updates?: string;-- mount?: string;-- [key: string]: unknown;--}--export interface ChromeOptions {-- /** Your app's id (brand and analytics). */-- service?: string;-- brand?: { name?: string; sub?: string; tld?: string; icon?: string; variant?: string };-- links?: ChromeLink[];-- menu?: { before?: ChromeMenuItem[]; after?: ChromeMenuItem[] };-- /** Same-origin endpoint answering { user } for your server session. */-- sessionUrl?: string;-- /** Sign-in URL; {url} = full return URL, {path} = its local path. */-- loginUrl?: string;-- /** Sign-out URL, so your server session ends too; {url} / {path} as for loginUrl. */-- logoutUrl?: string;-- silentLogin?: string;-- /** Footer options, or false for no footer. */-- footer?: ChromeFooterConfig | false;-- /** false: no "shipped X ago" line in the footer. */-- shipped?: boolean;-- /** false: do not load the shared theme loader. */-- theme?: boolean;-- /** Where the shared chrome is served (default https://openvibe.network). */-- base?: string;-- /** Extra navbar options passed through to OpenVibeNavbar.init. */-- navbar?: Record<string, unknown>;-- document?: unknown;-- window?: unknown;--}--export interface ChromeConfig { navbar: Record<string, unknown>; footer: Record<string, unknown> | null }--export interface ChromeUrls { theme: string; navbar: string; footer: string; shipped: string }----export const DEFAULT_BASE: string;--export function scriptUrls(opts?: { base?: string }): ChromeUrls;--export function chromeConfig(opts?: ChromeOptions): ChromeConfig;--export function chromeTags(opts?: ChromeOptions): { head: string; bodyStart: string; bodyEnd: string; config: ChromeConfig; urls: ChromeUrls };--export function mountChrome(opts?: ChromeOptions): Promise<{ navbar: unknown | null; footer: unknown | null }>;nnew file mode 100644@@@ -0,0 +1,46 @@++// openvibe-sdk/chrome — the OpenVibe network's shared navbar, footer, "shipped" views and theme loader for any app.++++export interface ChromeLink { label: string; href: string; icon?: string; active?: boolean }++export interface ChromeMenuItem { label: string; href?: string; icon?: string; onClick?: () => void; danger?: boolean; external?: boolean }++export interface ChromeFooterConfig {++ links?: Array<{ heading: string; items: Array<{ name: string; url: string }> }>;++ variant?: 'full' | 'compact';++ /** This app's update log; default: https://openvibe.network/updates?site=<host>. */++ updates?: string;++ mount?: string;++ [key: string]: unknown;++}++export interface ChromeOptions {++ /** Your app's id (brand and analytics). */++ service?: string;++ brand?: { name?: string; sub?: string; tld?: string; icon?: string; variant?: string };++ links?: ChromeLink[];++ menu?: { before?: ChromeMenuItem[]; after?: ChromeMenuItem[] };++ /** Same-origin endpoint answering { user } for your server session. */++ sessionUrl?: string;++ /** Sign-in URL; {url} = full return URL, {path} = its local path. */++ loginUrl?: string;++ /** Sign-out URL, so your server session ends too; {url} / {path} as for loginUrl. */++ logoutUrl?: string;++ silentLogin?: string;++ /** Footer options, or false for no footer. */++ footer?: ChromeFooterConfig | false;++ /** false: no "shipped X ago" line in the footer. */++ shipped?: boolean;++ /** false: do not load the shared theme loader. */++ theme?: boolean;++ /** Where the shared chrome is served (default https://openvibe.network). */++ base?: string;++ /** Extra navbar options passed through to OpenVibeNavbar.init. */++ navbar?: Record<string, unknown>;++ document?: unknown;++ window?: unknown;++}++export interface ChromeConfig { navbar: Record<string, unknown>; footer: Record<string, unknown> | null }++export interface ChromeUrls { theme: string; navbar: string; footer: string; shipped: string }++++export const DEFAULT_BASE: string;++export function scriptUrls(opts?: { base?: string }): ChromeUrls;++export function chromeConfig(opts?: ChromeOptions): ChromeConfig;++export function chromeTags(opts?: ChromeOptions): { head: string; bodyStart: string; bodyEnd: string; config: ChromeConfig; urls: ChromeUrls };++export function mountChrome(opts?: ChromeOptions): Promise<{ navbar: unknown | null; footer: unknown | null }>;
@@ -5,7 +5,7 @@ release may change an API and says so here.
@@ -5,7 +5,7 @@ release may change an API and says so here.
## 0.8.0 (2026-09-24)
## 0.8.0 (2026-09-24)
-`openvibe-sdk/chrome`: the OpenVibe network's shared chrome for any app, first-party or not. It covers:
+`openvibe-sdk/frame`: **the OpenVibe Frame**, the navbar, footer and themes every OpenVibe site sits in, for any app, first-party or not. It covers:
- the universal navbar: one account menu, notifications, the site switcher, and sign in and out through your own session endpoints (`sessionUrl`, `loginUrl`, `logoutUrl` with `{path}`/`{url}`);
- the universal navbar: one account menu, notifications, the site switcher, and sign in and out through your own session endpoints (`sessionUrl`, `loginUrl`, `logoutUrl` with `{path}`/`{url}`);
- the shared footer, with network and legal links, the "shipped X ago" line and an Updates link;
- the shared footer, with network and legal links, the "shipped X ago" line and an Updates link;
@@ -13,7 +13,7 @@ release may change an API and says so here.
@@ -13,7 +13,7 @@ release may change an API and says so here.
These are the same files every OpenVibe site runs, served by OpenVibe.Network from the published openvibe-shared release, so nothing is bundled into your app.
These are the same files every OpenVibe site runs, served by OpenVibe.Network from the published openvibe-shared release, so nothing is bundled into your app.
-`mountChrome(opts)` loads and mounts them in the browser. It never throws for a network failure; a part that cannot load resolves to `null`. `chromeTags(opts)` returns the same markup as strings for server-rendered pages, with the configuration escaped for its `<script>`. `chromeConfig` and `scriptUrls` are exported for custom setups. The base must be an https origin (or `http://localhost` in development). Browser-safe; no dependencies.
+`mountFrame(opts)` loads and mounts them in the browser. It never throws for a network failure; a part that cannot load resolves to `null`. `frameTags(opts)` returns the same markup as strings for server-rendered pages, with the configuration escaped for its `<script>`. `frameConfig` and `scriptUrls` are exported for custom setups. The base must be an https origin (or `http://localhost` in development). Browser-safe; no dependencies.
## 0.7.0 (2026-09-24)
## 0.7.0 (2026-09-24)
@@ -237,13 +237,13 @@ app.post('/internal/events', express.raw({ type: 'application/json' }), (req, re
@@ -237,13 +237,13 @@ app.post('/internal/events', express.raw({ type: 'application/json' }), (req, re
Turn `requireV2` on once Events sends v2 to you: a replayed v1-only delivery then fails. The lower-level checks are `verifyDeliveryV2(raw, headers, secret, { toleranceSec, now })` (constant-time; also refuses a `X-OpenVibe-Timestamp` that differs from `t`) and `verifyDelivery(raw, signatureHeader, secret)` (v1, unchanged). Keep your clock in sync (NTP): the window is ±300 s both ways. In tests, `signDeliveryHeaders(raw, secret, { now })` returns the three headers as Events sends them.
Turn `requireV2` on once Events sends v2 to you: a replayed v1-only delivery then fails. The lower-level checks are `verifyDeliveryV2(raw, headers, secret, { toleranceSec, now })` (constant-time; also refuses a `X-OpenVibe-Timestamp` that differs from `t`) and `verifyDelivery(raw, signatureHeader, secret)` (v1, unchanged). Keep your clock in sync (NTP): the window is ±300 s both ways. In tests, `signDeliveryHeaders(raw, secret, { now })` returns the three headers as Events sends them.
-### The shared chrome
+### The OpenVibe Frame
Every OpenVibe site uses the same navbar and footer. Your app can use them too, so people move between it and the network without a jump:
Every OpenVibe site uses the same navbar and footer. Your app can use them too, so people move between it and the network without a jump:
```js
```js
-// Browser (CommonJS or ESM: openvibe-sdk/chrome)
-const { navbar } = await mountChrome({
+// Browser (CommonJS or ESM: openvibe-sdk/frame)
+const { navbar } = await mountFrame({
service: 'myapp',
service: 'myapp',
brand: { name: 'My App' },
brand: { name: 'My App' },
links: [{ label: 'Home', href: '/' }, { label: 'Docs', href: '/docs' }],
links: [{ label: 'Home', href: '/' }, { label: 'Docs', href: '/docs' }],
@@ -255,9 +255,9 @@ const { navbar } = await mountChrome({
@@ -255,9 +255,9 @@ const { navbar } = await mountChrome({
});
});
```
```
-For server-rendered pages, `chromeTags(opts)` returns `{ head, bodyStart, bodyEnd }`: put `head` in `<head>`, `bodyStart` first in `<body>` and `bodyEnd` last. Your CSP needs `script-src https://openvibe.network` and `connect-src https://openvibe.network`.
+For server-rendered pages, `frameTags(opts)` returns `{ head, bodyStart, bodyEnd }`: put `head` in `<head>`, `bodyStart` first in `<body>` and `bodyEnd` last. Your CSP needs `script-src https://openvibe.network` and `connect-src https://openvibe.network`.
-The chrome is progressive: if openvibe.network cannot be reached, your page still renders, and the part that failed resolves to `null`. Signed-in state comes from the shared `ov_token`, and otherwise from your `sessionUrl`. To have your users recognised across the network, sign them in with OpenVibe (`openvibe-sdk/auth`).
+The Frame is progressive: if openvibe.network cannot be reached, your page still renders, and the part that failed resolves to `null`. Signed-in state comes from the shared `ov_token`, and otherwise from your `sessionUrl`. To have your users recognised across the network, sign them in with OpenVibe (`openvibe-sdk/auth`).
### Tools
### Tools
deleted file mode 100644
deleted file mode 100644
@@ -1,7 +0,0 @@
@@ -1,7 +0,0 @@
-// Generated by scripts/esm.js from src/chrome.js. Do not edit.
-import cjs from '../src/chrome.js';
-
-export const {
- mountChrome, chromeTags, chromeConfig, scriptUrls, DEFAULT_BASE,
-} = cjs;
-export default cjs;
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+// Generated by scripts/esm.js from src/chrome.js. Do not edit.
+import cjs from '../src/chrome.js';
+
+export const {
+ mountChrome, chromeTags, chromeConfig, scriptUrls, DEFAULT_BASE,
+} = cjs;
+export default cjs;
@@ -80,10 +80,10 @@
@@ -80,10 +80,10 @@
"import": "./esm/projects.mjs",
"import": "./esm/projects.mjs",
"default": "./src/projects.js"
"default": "./src/projects.js"
},
},
- "./chrome": {
- "types": "./types/chrome.d.ts",
- "import": "./esm/chrome.mjs",
- "default": "./src/chrome.js"
+ "./frame": {
+ "types": "./types/frame.d.ts",
+ "import": "./esm/frame.mjs",
+ "default": "./src/frame.js"
},
},
"./vip": {
"./vip": {
"types": "./types/vip.d.ts",
"types": "./types/vip.d.ts",
@@ -27,7 +27,7 @@ const ENTRIES = {
@@ -27,7 +27,7 @@ const ENTRIES = {
'tools.mjs': '../src/tools.js',
'tools.mjs': '../src/tools.js',
'projects.mjs': '../src/projects.js',
'projects.mjs': '../src/projects.js',
'vip.mjs': '../src/vip.js',
'vip.mjs': '../src/vip.js',
- 'chrome.mjs': '../src/chrome.js',
+ 'frame.mjs': '../src/frame.js',
'testing.mjs': '../src/testing/index.js',
'testing.mjs': '../src/testing/index.js',
};
};
deleted file mode 100644
deleted file mode 100644
@@ -1,150 +0,0 @@
@@ -1,150 +0,0 @@
-'use strict';
-
-/**
- * openvibe-sdk/chrome — the OpenVibe network's shared chrome for any app: the universal navbar (one
- * account menu, notifications, the site switcher, sign in and out), the shared footer (network
- * links, legal links, the "shipped X ago" line and an Updates link), the "shipped" views and the
- * theme loader. These are the same files every OpenVibe site runs, served by OpenVibe.Network from
- * the published openvibe-shared release. No dependencies; nothing is bundled into your app.
- *
- * In the browser:
- *
- * // mountChrome from the openvibe-sdk/chrome entry (CommonJS or ESM)
- * const { navbar, footer } = await mountChrome({
- * service: 'myapp', // your app's id (brand + analytics)
- * brand: { name: 'My App' }, // optional brand override
- * links: [{ label: 'Home', href: '/' }, { label: 'Docs', href: '/docs' }],
- * menu: { before: [{ label: 'My projects', href: '/projects', icon: 'fa-folder' }] },
- * sessionUrl: '/auth/me', // your server session ({ user }), optional
- * loginUrl: '/auth/login?next={path}', // {url} full return URL, {path} local path
- * logoutUrl: '/auth/logout?next={path}', // Sign out ends your session too
- * footer: { links: [{ heading: 'My App', items: [{ name: 'About', url: '/about' }] }], updates: '/updates' },
- * });
- *
- * Server-rendered apps put chromeTags(opts) in <head>/<body> instead (the same markup this module
- * would inject, as a string; the JSON config is escaped for a <script> element):
- *
- * // chromeTags from the openvibe-sdk/chrome entry
- * const t = chromeTags({ service: 'myapp', links: [...] });
- * html = `<head>${t.head}</head><body>${t.bodyStart}…${t.bodyEnd}</body>`;
- *
- * Your CSP must allow `script-src https://openvibe.network` and `connect-src https://openvibe.network`
- * (the navbar asks /api/auth/me and the notifications there; the shipped line reads
- * /api/v1/changelog). Signed-in state comes from the shared ov_token, else from sessionUrl. For
- * your own users to be recognised across the network, sign them in with OpenVibe (openvibe-sdk/auth).
- * The chrome is progressive: if openvibe.network is unreachable your page still renders.
- */
-
-const DEFAULT_BASE = 'https://openvibe.network';
-const FILES = { theme: 'theme-loader.js', navbar: 'navbar.js', footer: 'footer.js', shipped: 'shipped.js' };
-
-function baseOf(base) {
- const b = String(base || DEFAULT_BASE).replace(/\/+$/, '');
- const ok = /^https:\/\/[a-z0-9.-]+(:\d+)?$/i.test(b) || /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(b);
- if (!ok) throw new TypeError(`openvibe-sdk/chrome: base must be an https origin (or http://localhost for development), got ${base}`);
- return b;
-}
-
-/** The script URLs on a base: the release OpenVibe.Network currently serves at /shared/. */
-function scriptUrls({ base } = {}) {
- const b = baseOf(base);
- const out = {};
- for (const [k, f] of Object.entries(FILES)) out[k] = `${b}/shared/${f}`;
- return out;
-}
-
-/** The navbar and footer configurations mountChrome passes to OpenVibeNavbar.init / OpenVibeFooter.init. */
-function chromeConfig(opts = {}) {
- const o = opts || {};
- const b = baseOf(o.base);
- const navbar = {
- service: o.service || 'app',
- apiBase: b,
- ...(o.brand ? { brand: o.brand } : {}),
- ...(Array.isArray(o.links) ? { links: o.links } : {}),
- ...(o.menu ? { menu: o.menu } : {}),
- ...(o.sessionUrl ? { sessionUrl: o.sessionUrl } : {}),
- ...(o.loginUrl ? { loginUrl: o.loginUrl } : {}),
- ...(o.logoutUrl ? { logoutUrl: o.logoutUrl } : {}),
- ...(o.silentLogin ? { silentLogin: o.silentLogin } : {}),
- ...(o.navbar && typeof o.navbar === 'object' ? o.navbar : {}),
- };
- const footer = o.footer === false ? null : {
- service: o.service || 'app',
- mount: '#ov-footer',
- variant: 'full',
- apiBase: b,
- ...(o.brand && o.brand.name ? { brandName: o.brand.name } : {}),
- ...(o.shipped === false ? { shipped: false } : {}),
- ...(o.footer && typeof o.footer === 'object' ? o.footer : {}),
- };
- return { navbar, footer };
-}
-
-const jsonForScript = (v) => JSON.stringify(v).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
-const attr = (s) => String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
-
-/** Markup for server-rendered pages: { head, bodyStart, bodyEnd } strings. */
-function chromeTags(opts = {}) {
- const urls = scriptUrls(opts);
- const cfg = chromeConfig(opts);
- const head = [
- opts.theme === false ? '' : `<script src="${attr(urls.theme)}" defer></script>`,
- `<script src="${attr(urls.navbar)}" defer></script>`,
- cfg.footer ? `<script src="${attr(urls.footer)}" defer></script>` : '',
- ].filter(Boolean).join('\n');
- const bodyStart = '<div id="navbar-mount"></div>';
- const init = `document.addEventListener('DOMContentLoaded',function(){var c=${jsonForScript(cfg)};`
- + `try{if(window.OpenVibeNavbar)OpenVibeNavbar.init(c.navbar);}catch(e){}`
- + `try{if(c.footer&&window.OpenVibeFooter)OpenVibeFooter.init(c.footer);}catch(e){}});`;
- const bodyEnd = `${cfg.footer ? '<footer id="ov-footer"></footer>\n' : ''}<script>${init}</script>`;
- return { head, bodyStart, bodyEnd, config: cfg, urls };
-}
-
-function loadScript(doc, src) {
- return new Promise((resolve) => {
- const existing = doc.querySelector(`script[data-ov-chrome="${src}"]`);
- if (existing) { if (existing.dataset.loaded) resolve(true); else existing.addEventListener('load', () => resolve(true)); return; }
- const s = doc.createElement('script');
- s.src = src;
- s.async = true;
- s.setAttribute('data-ov-chrome', src);
- s.addEventListener('load', () => { s.dataset.loaded = '1'; resolve(true); });
- s.addEventListener('error', () => resolve(false));
- (doc.head || doc.documentElement).appendChild(s);
- });
-}
-
-/**
- * Browser: load the shared chrome and mount it. Resolves { navbar, footer } (the initialised
- * globals, or null for a part that could not load). Never throws for a network failure.
- */
-async function mountChrome(opts = {}) {
- const doc = (opts && opts.document) || (typeof document !== 'undefined' ? document : null);
- const win = (opts && opts.window) || (typeof window !== 'undefined' ? window : null);
- if (!doc || !win) throw new Error('openvibe-sdk/chrome: mountChrome runs in a browser; use chromeTags() on the server');
- const urls = scriptUrls(opts);
- const cfg = chromeConfig(opts);
- if (!doc.getElementById('navbar-mount')) {
- const m = doc.createElement('div');
- m.id = 'navbar-mount';
- doc.body.insertBefore(m, doc.body.firstChild);
- }
- if (cfg.footer && !doc.querySelector(cfg.footer.mount)) {
- const f = doc.createElement('footer');
- f.id = String(cfg.footer.mount).replace(/^#/, '');
- doc.body.appendChild(f);
- }
- const loads = [];
- if (opts.theme !== false) loads.push(loadScript(doc, urls.theme));
- const navOk = loadScript(doc, urls.navbar);
- const footOk = cfg.footer ? loadScript(doc, urls.footer) : Promise.resolve(false);
- await Promise.all([...loads, navOk, footOk]);
- let navbar = null;
- let footer = null;
- try { if (win.OpenVibeNavbar) { win.OpenVibeNavbar.init(cfg.navbar); navbar = win.OpenVibeNavbar; } } catch { navbar = null; }
- try { if (cfg.footer && win.OpenVibeFooter) { win.OpenVibeFooter.init(cfg.footer); footer = win.OpenVibeFooter; } } catch { footer = null; }
- return { navbar, footer };
-}
-
-module.exports = { mountChrome, chromeTags, chromeConfig, scriptUrls, DEFAULT_BASE };
new file mode 100644
new file mode 100644
@@ -0,0 +1,150 @@
@@ -0,0 +1,150 @@
+'use strict';
+
+/**
+ * openvibe-sdk/chrome — the OpenVibe network's shared chrome for any app: the universal navbar (one
+ * account menu, notifications, the site switcher, sign in and out), the shared footer (network
+ * links, legal links, the "shipped X ago" line and an Updates link), the "shipped" views and the
+ * theme loader. These are the same files every OpenVibe site runs, served by OpenVibe.Network from
+ * the published openvibe-shared release. No dependencies; nothing is bundled into your app.
+ *
+ * In the browser:
+ *
+ * // mountChrome from the openvibe-sdk/chrome entry (CommonJS or ESM)
+ * const { navbar, footer } = await mountChrome({
+ * service: 'myapp', // your app's id (brand + analytics)
+ * brand: { name: 'My App' }, // optional brand override
+ * links: [{ label: 'Home', href: '/' }, { label: 'Docs', href: '/docs' }],
+ * menu: { before: [{ label: 'My projects', href: '/projects', icon: 'fa-folder' }] },
+ * sessionUrl: '/auth/me', // your server session ({ user }), optional
+ * loginUrl: '/auth/login?next={path}', // {url} full return URL, {path} local path
+ * logoutUrl: '/auth/logout?next={path}', // Sign out ends your session too
+ * footer: { links: [{ heading: 'My App', items: [{ name: 'About', url: '/about' }] }], updates: '/updates' },
+ * });
+ *
+ * Server-rendered apps put chromeTags(opts) in <head>/<body> instead (the same markup this module
+ * would inject, as a string; the JSON config is escaped for a <script> element):
+ *
+ * // chromeTags from the openvibe-sdk/chrome entry
+ * const t = chromeTags({ service: 'myapp', links: [...] });
+ * html = `<head>${t.head}</head><body>${t.bodyStart}…${t.bodyEnd}</body>`;
+ *
+ * Your CSP must allow `script-src https://openvibe.network` and `connect-src https://openvibe.network`
+ * (the navbar asks /api/auth/me and the notifications there; the shipped line reads
+ * /api/v1/changelog). Signed-in state comes from the shared ov_token, else from sessionUrl. For
+ * your own users to be recognised across the network, sign them in with OpenVibe (openvibe-sdk/auth).
+ * The chrome is progressive: if openvibe.network is unreachable your page still renders.
+ */
+
+const DEFAULT_BASE = 'https://openvibe.network';
+const FILES = { theme: 'theme-loader.js', navbar: 'navbar.js', footer: 'footer.js', shipped: 'shipped.js' };
+
+function baseOf(base) {
+ const b = String(base || DEFAULT_BASE).replace(/\/+$/, '');
+ const ok = /^https:\/\/[a-z0-9.-]+(:\d+)?$/i.test(b) || /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(b);
+ if (!ok) throw new TypeError(`openvibe-sdk/chrome: base must be an https origin (or http://localhost for development), got ${base}`);
+ return b;
+}
+
+/** The script URLs on a base: the release OpenVibe.Network currently serves at /shared/. */
+function scriptUrls({ base } = {}) {
+ const b = baseOf(base);
+ const out = {};
+ for (const [k, f] of Object.entries(FILES)) out[k] = `${b}/shared/${f}`;
+ return out;
+}
+
+/** The navbar and footer configurations mountChrome passes to OpenVibeNavbar.init / OpenVibeFooter.init. */
+function chromeConfig(opts = {}) {
+ const o = opts || {};
+ const b = baseOf(o.base);
+ const navbar = {
+ service: o.service || 'app',
+ apiBase: b,
+ ...(o.brand ? { brand: o.brand } : {}),
+ ...(Array.isArray(o.links) ? { links: o.links } : {}),
+ ...(o.menu ? { menu: o.menu } : {}),
+ ...(o.sessionUrl ? { sessionUrl: o.sessionUrl } : {}),
+ ...(o.loginUrl ? { loginUrl: o.loginUrl } : {}),
+ ...(o.logoutUrl ? { logoutUrl: o.logoutUrl } : {}),
+ ...(o.silentLogin ? { silentLogin: o.silentLogin } : {}),
+ ...(o.navbar && typeof o.navbar === 'object' ? o.navbar : {}),
+ };
+ const footer = o.footer === false ? null : {
+ service: o.service || 'app',
+ mount: '#ov-footer',
+ variant: 'full',
+ apiBase: b,
+ ...(o.brand && o.brand.name ? { brandName: o.brand.name } : {}),
+ ...(o.shipped === false ? { shipped: false } : {}),
+ ...(o.footer && typeof o.footer === 'object' ? o.footer : {}),
+ };
+ return { navbar, footer };
+}
+
+const jsonForScript = (v) => JSON.stringify(v).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
+const attr = (s) => String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
+
+/** Markup for server-rendered pages: { head, bodyStart, bodyEnd } strings. */
+function chromeTags(opts = {}) {
+ const urls = scriptUrls(opts);
+ const cfg = chromeConfig(opts);
+ const head = [
+ opts.theme === false ? '' : `<script src="${attr(urls.theme)}" defer></script>`,
+ `<script src="${attr(urls.navbar)}" defer></script>`,
+ cfg.footer ? `<script src="${attr(urls.footer)}" defer></script>` : '',
+ ].filter(Boolean).join('\n');
+ const bodyStart = '<div id="navbar-mount"></div>';
+ const init = `document.addEventListener('DOMContentLoaded',function(){var c=${jsonForScript(cfg)};`
+ + `try{if(window.OpenVibeNavbar)OpenVibeNavbar.init(c.navbar);}catch(e){}`
+ + `try{if(c.footer&&window.OpenVibeFooter)OpenVibeFooter.init(c.footer);}catch(e){}});`;
+ const bodyEnd = `${cfg.footer ? '<footer id="ov-footer"></footer>\n' : ''}<script>${init}</script>`;
+ return { head, bodyStart, bodyEnd, config: cfg, urls };
+}
+
+function loadScript(doc, src) {
+ return new Promise((resolve) => {
+ const existing = doc.querySelector(`script[data-ov-chrome="${src}"]`);
+ if (existing) { if (existing.dataset.loaded) resolve(true); else existing.addEventListener('load', () => resolve(true)); return; }
+ const s = doc.createElement('script');
+ s.src = src;
+ s.async = true;
+ s.setAttribute('data-ov-chrome', src);
+ s.addEventListener('load', () => { s.dataset.loaded = '1'; resolve(true); });
+ s.addEventListener('error', () => resolve(false));
+ (doc.head || doc.documentElement).appendChild(s);
+ });
+}
+
+/**
+ * Browser: load the shared chrome and mount it. Resolves { navbar, footer } (the initialised
+ * globals, or null for a part that could not load). Never throws for a network failure.
+ */
+async function mountChrome(opts = {}) {
+ const doc = (opts && opts.document) || (typeof document !== 'undefined' ? document : null);
+ const win = (opts && opts.window) || (typeof window !== 'undefined' ? window : null);
+ if (!doc || !win) throw new Error('openvibe-sdk/chrome: mountChrome runs in a browser; use chromeTags() on the server');
+ const urls = scriptUrls(opts);
+ const cfg = chromeConfig(opts);
+ if (!doc.getElementById('navbar-mount')) {
+ const m = doc.createElement('div');
+ m.id = 'navbar-mount';
+ doc.body.insertBefore(m, doc.body.firstChild);
+ }
+ if (cfg.footer && !doc.querySelector(cfg.footer.mount)) {
+ const f = doc.createElement('footer');
+ f.id = String(cfg.footer.mount).replace(/^#/, '');
+ doc.body.appendChild(f);
+ }
+ const loads = [];
+ if (opts.theme !== false) loads.push(loadScript(doc, urls.theme));
+ const navOk = loadScript(doc, urls.navbar);
+ const footOk = cfg.footer ? loadScript(doc, urls.footer) : Promise.resolve(false);
+ await Promise.all([...loads, navOk, footOk]);
+ let navbar = null;
+ let footer = null;
+ try { if (win.OpenVibeNavbar) { win.OpenVibeNavbar.init(cfg.navbar); navbar = win.OpenVibeNavbar; } } catch { navbar = null; }
+ try { if (cfg.footer && win.OpenVibeFooter) { win.OpenVibeFooter.init(cfg.footer); footer = win.OpenVibeFooter; } } catch { footer = null; }
+ return { navbar, footer };
+}
+
+module.exports = { mountChrome, chromeTags, chromeConfig, scriptUrls, DEFAULT_BASE };
deleted file mode 100644
deleted file mode 100644
@@ -1,84 +0,0 @@
@@ -1,84 +0,0 @@
-'use strict';
-// openvibe-sdk/chrome: any app mounts the network's shared navbar, footer, shipped line and themes,
-// with its own session endpoints; server-rendered apps get the same markup as strings.
-const assert = require('node:assert/strict');
-const { mountChrome, chromeTags, chromeConfig, scriptUrls } = require('../src/chrome');
-
-const opts = {
- service: 'myapp',
- brand: { name: 'My App' },
- links: [{ label: 'Docs </script><script>alert(1)</script>', href: '/docs' }],
- menu: { before: [{ label: 'My projects', href: '/projects' }] },
- sessionUrl: '/auth/me', loginUrl: '/auth/login?next={path}', logoutUrl: '/auth/logout?next={path}',
- footer: { updates: '/updates' },
-};
-
-// Config mapping
-const cfg = chromeConfig(opts);
-assert.equal(cfg.navbar.service, 'myapp');
-assert.equal(cfg.navbar.apiBase, 'https://openvibe.network');
-assert.equal(cfg.navbar.logoutUrl, '/auth/logout?next={path}');
-assert.deepEqual(cfg.navbar.menu.before[0], { label: 'My projects', href: '/projects' });
-assert.equal(cfg.footer.updates, '/updates');
-assert.equal(cfg.footer.brandName, 'My App');
-assert.equal(chromeConfig({ footer: false }).footer, null);
-assert.equal(chromeConfig({ shipped: false }).footer.shipped, false);
-
-// URLs and base validation
-assert.equal(scriptUrls().navbar, 'https://openvibe.network/shared/navbar.js');
-assert.equal(scriptUrls({ base: 'http://localhost:4000/' }).footer, 'http://localhost:4000/shared/footer.js');
-assert.throws(() => scriptUrls({ base: 'http://evil.example' }), /https origin/);
-assert.throws(() => scriptUrls({ base: 'javascript:alert(1)' }), /https origin/);
-
-// SSR tags: the config cannot break out of its <script>
-const t = chromeTags(opts);
-assert.ok(t.head.includes('https://openvibe.network/shared/navbar.js') && t.head.includes('theme-loader.js') && t.head.includes('footer.js'));
-assert.equal(t.bodyStart, '<div id="navbar-mount"></div>');
-assert.ok(t.bodyEnd.includes('<footer id="ov-footer"></footer>'));
-assert.ok(!/<\/script><script>alert/.test(t.bodyEnd), 'a label cannot close the init script');
-assert.equal((t.bodyEnd.match(/<\/script>/g) || []).length, 1);
-assert.ok(!chromeTags({ theme: false }).head.includes('theme-loader'));
-
-// Browser mount with a minimal DOM: scripts "load", globals appear, init receives the config.
-function fakeDom() {
- const inits = {};
- const win = {};
- const nodes = [];
- const el = (tag) => {
- const n = { tagName: tag.toUpperCase(), id: '', dataset: {}, attrs: {}, listeners: {}, children: [],
- setAttribute(k, v) { this.attrs[k] = v; }, addEventListener(ev, fn) { (this.listeners[ev] = this.listeners[ev] || []).push(fn); },
- appendChild(c) { this.children.push(c); nodes.push(c); if (c.tagName === 'SCRIPT') setImmediate(() => {
- if (c.src.endsWith('navbar.js')) win.OpenVibeNavbar = { init: (x) => { inits.navbar = x; } };
- if (c.src.endsWith('footer.js')) win.OpenVibeFooter = { init: (x) => { inits.footer = x; } };
- (c.listeners.load || []).forEach((f) => f());
- }); return c; },
- insertBefore(c) { nodes.push(c); return c; } };
- return n;
- };
- const doc = {
- head: el('head'), body: el('body'), documentElement: el('html'),
- createElement: el,
- getElementById: (id) => nodes.find((n) => n.id === id) || null,
- querySelector: (sel) => sel.startsWith('#') ? nodes.find((n) => n.id === sel.slice(1)) || null : nodes.find((n) => n.tagName === 'SCRIPT' && sel.includes(n.src)) || null,
- };
- doc.body.firstChild = null;
- return { doc, win, inits, nodes };
-}
-
-(async () => {
- const d = fakeDom();
- const r = await mountChrome({ ...opts, document: d.doc, window: d.win });
- assert.ok(r.navbar && r.footer);
- assert.equal(d.inits.navbar.sessionUrl, '/auth/me');
- assert.equal(d.inits.footer.mount, '#ov-footer');
- assert.ok(d.doc.getElementById('navbar-mount') && d.doc.getElementById('ov-footer'), 'mount points created');
- assert.deepEqual(d.nodes.filter((n) => n.tagName === 'SCRIPT').map((n) => n.src.split('/').pop()).sort(), ['footer.js', 'navbar.js', 'theme-loader.js']);
-
- // A failed load leaves that part null and never throws.
- const d2 = fakeDom();
- d2.doc.head.appendChild = function (c) { d2.nodes.push(c); setImmediate(() => (c.listeners.error || []).forEach((f) => f())); return c; };
- const r2 = await mountChrome({ document: d2.doc, window: d2.win });
- assert.deepEqual(r2, { navbar: null, footer: null });
- await assert.rejects(() => mountChrome({ document: null, window: null }), /browser/);
- console.log('chrome: all checks passed');
-})().catch((e) => { console.error(e); process.exit(1); });
new file mode 100644
new file mode 100644
@@ -0,0 +1,84 @@
@@ -0,0 +1,84 @@
+'use strict';
+// openvibe-sdk/chrome: any app mounts the network's shared navbar, footer, shipped line and themes,
+// with its own session endpoints; server-rendered apps get the same markup as strings.
+const assert = require('node:assert/strict');
+const { mountChrome, chromeTags, chromeConfig, scriptUrls } = require('../src/chrome');
+
+const opts = {
+ service: 'myapp',
+ brand: { name: 'My App' },
+ links: [{ label: 'Docs </script><script>alert(1)</script>', href: '/docs' }],
+ menu: { before: [{ label: 'My projects', href: '/projects' }] },
+ sessionUrl: '/auth/me', loginUrl: '/auth/login?next={path}', logoutUrl: '/auth/logout?next={path}',
+ footer: { updates: '/updates' },
+};
+
+// Config mapping
+const cfg = chromeConfig(opts);
+assert.equal(cfg.navbar.service, 'myapp');
+assert.equal(cfg.navbar.apiBase, 'https://openvibe.network');
+assert.equal(cfg.navbar.logoutUrl, '/auth/logout?next={path}');
+assert.deepEqual(cfg.navbar.menu.before[0], { label: 'My projects', href: '/projects' });
+assert.equal(cfg.footer.updates, '/updates');
+assert.equal(cfg.footer.brandName, 'My App');
+assert.equal(chromeConfig({ footer: false }).footer, null);
+assert.equal(chromeConfig({ shipped: false }).footer.shipped, false);
+
+// URLs and base validation
+assert.equal(scriptUrls().navbar, 'https://openvibe.network/shared/navbar.js');
+assert.equal(scriptUrls({ base: 'http://localhost:4000/' }).footer, 'http://localhost:4000/shared/footer.js');
+assert.throws(() => scriptUrls({ base: 'http://evil.example' }), /https origin/);
+assert.throws(() => scriptUrls({ base: 'javascript:alert(1)' }), /https origin/);
+
+// SSR tags: the config cannot break out of its <script>
+const t = chromeTags(opts);
+assert.ok(t.head.includes('https://openvibe.network/shared/navbar.js') && t.head.includes('theme-loader.js') && t.head.includes('footer.js'));
+assert.equal(t.bodyStart, '<div id="navbar-mount"></div>');
+assert.ok(t.bodyEnd.includes('<footer id="ov-footer"></footer>'));
+assert.ok(!/<\/script><script>alert/.test(t.bodyEnd), 'a label cannot close the init script');
+assert.equal((t.bodyEnd.match(/<\/script>/g) || []).length, 1);
+assert.ok(!chromeTags({ theme: false }).head.includes('theme-loader'));
+
+// Browser mount with a minimal DOM: scripts "load", globals appear, init receives the config.
+function fakeDom() {
+ const inits = {};
+ const win = {};
+ const nodes = [];
+ const el = (tag) => {
+ const n = { tagName: tag.toUpperCase(), id: '', dataset: {}, attrs: {}, listeners: {}, children: [],
+ setAttribute(k, v) { this.attrs[k] = v; }, addEventListener(ev, fn) { (this.listeners[ev] = this.listeners[ev] || []).push(fn); },
+ appendChild(c) { this.children.push(c); nodes.push(c); if (c.tagName === 'SCRIPT') setImmediate(() => {
+ if (c.src.endsWith('navbar.js')) win.OpenVibeNavbar = { init: (x) => { inits.navbar = x; } };
+ if (c.src.endsWith('footer.js')) win.OpenVibeFooter = { init: (x) => { inits.footer = x; } };
+ (c.listeners.load || []).forEach((f) => f());
+ }); return c; },
+ insertBefore(c) { nodes.push(c); return c; } };
+ return n;
+ };
+ const doc = {
+ head: el('head'), body: el('body'), documentElement: el('html'),
+ createElement: el,
+ getElementById: (id) => nodes.find((n) => n.id === id) || null,
+ querySelector: (sel) => sel.startsWith('#') ? nodes.find((n) => n.id === sel.slice(1)) || null : nodes.find((n) => n.tagName === 'SCRIPT' && sel.includes(n.src)) || null,
+ };
+ doc.body.firstChild = null;
+ return { doc, win, inits, nodes };
+}
+
+(async () => {
+ const d = fakeDom();
+ const r = await mountChrome({ ...opts, document: d.doc, window: d.win });
+ assert.ok(r.navbar && r.footer);
+ assert.equal(d.inits.navbar.sessionUrl, '/auth/me');
+ assert.equal(d.inits.footer.mount, '#ov-footer');
+ assert.ok(d.doc.getElementById('navbar-mount') && d.doc.getElementById('ov-footer'), 'mount points created');
+ assert.deepEqual(d.nodes.filter((n) => n.tagName === 'SCRIPT').map((n) => n.src.split('/').pop()).sort(), ['footer.js', 'navbar.js', 'theme-loader.js']);
+
+ // A failed load leaves that part null and never throws.
+ const d2 = fakeDom();
+ d2.doc.head.appendChild = function (c) { d2.nodes.push(c); setImmediate(() => (c.listeners.error || []).forEach((f) => f())); return c; };
+ const r2 = await mountChrome({ document: d2.doc, window: d2.win });
+ assert.deepEqual(r2, { navbar: null, footer: null });
+ await assert.rejects(() => mountChrome({ document: null, window: null }), /browser/);
+ console.log('chrome: all checks passed');
+})().catch((e) => { console.error(e); process.exit(1); });
@@ -22,7 +22,7 @@ run([
@@ -22,7 +22,7 @@ run([
const out = execFileSync(npm, ['pack', '--json', '--pack-destination', tmp], { cwd: ROOT, env, encoding: 'utf8' });
const out = execFileSync(npm, ['pack', '--json', '--pack-destination', tmp], { cwd: ROOT, env, encoding: 'utf8' });
const info = JSON.parse(out)[0];
const info = JSON.parse(out)[0];
const files = info.files.map((f) => f.path);
const files = info.files.map((f) => f.path);
- for (const must of ['package.json', 'index.js', 'browser.js', 'LICENSE', 'README.md', 'CHANGELOG.md', 'src/core/client.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'src/vip.js', 'esm/vip.mjs', 'types/vip.d.ts', 'src/chrome.js', 'esm/chrome.mjs', 'types/chrome.d.ts', 'esm/core.mjs', 'esm/jobs.mjs', 'esm/tools.mjs', 'types/tools.d.ts', 'types/core.d.ts', 'types/contracts.d.ts', 'types/bundle.d.ts', 'browser/openvibe-sdk.mjs']) {
+ for (const must of ['package.json', 'index.js', 'browser.js', 'LICENSE', 'README.md', 'CHANGELOG.md', 'src/core/client.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'src/vip.js', 'esm/vip.mjs', 'types/vip.d.ts', 'src/frame.js', 'esm/frame.mjs', 'types/frame.d.ts', 'esm/core.mjs', 'esm/jobs.mjs', 'esm/tools.mjs', 'types/tools.d.ts', 'types/core.d.ts', 'types/contracts.d.ts', 'types/bundle.d.ts', 'browser/openvibe-sdk.mjs']) {
assert.ok(files.includes(must), `tarball contains ${must}`);
assert.ok(files.includes(must), `tarball contains ${must}`);
}
}
assert.ok(!files.some((f) => f.startsWith('test/') || f.startsWith('scripts/') || f.startsWith('.github/')), 'no tests or tooling in the tarball');
assert.ok(!files.some((f) => f.startsWith('test/') || f.startsWith('scripts/') || f.startsWith('.github/')), 'no tests or tooling in the tarball');
deleted file mode 100644
deleted file mode 100644
@@ -1,46 +0,0 @@
@@ -1,46 +0,0 @@
-// openvibe-sdk/chrome — the OpenVibe network's shared navbar, footer, "shipped" views and theme loader for any app.
-
-export interface ChromeLink { label: string; href: string; icon?: string; active?: boolean }
-export interface ChromeMenuItem { label: string; href?: string; icon?: string; onClick?: () => void; danger?: boolean; external?: boolean }
-export interface ChromeFooterConfig {
- links?: Array<{ heading: string; items: Array<{ name: string; url: string }> }>;
- variant?: 'full' | 'compact';
- /** This app's update log; default: https://openvibe.network/updates?site=<host>. */
- updates?: string;
- mount?: string;
- [key: string]: unknown;
-}
-export interface ChromeOptions {
- /** Your app's id (brand and analytics). */
- service?: string;
- brand?: { name?: string; sub?: string; tld?: string; icon?: string; variant?: string };
- links?: ChromeLink[];
- menu?: { before?: ChromeMenuItem[]; after?: ChromeMenuItem[] };
- /** Same-origin endpoint answering { user } for your server session. */
- sessionUrl?: string;
- /** Sign-in URL; {url} = full return URL, {path} = its local path. */
- loginUrl?: string;
- /** Sign-out URL, so your server session ends too; {url} / {path} as for loginUrl. */
- logoutUrl?: string;
- silentLogin?: string;
- /** Footer options, or false for no footer. */
- footer?: ChromeFooterConfig | false;
- /** false: no "shipped X ago" line in the footer. */
- shipped?: boolean;
- /** false: do not load the shared theme loader. */
- theme?: boolean;
- /** Where the shared chrome is served (default https://openvibe.network). */
- base?: string;
- /** Extra navbar options passed through to OpenVibeNavbar.init. */
- navbar?: Record<string, unknown>;
- document?: unknown;
- window?: unknown;
-}
-export interface ChromeConfig { navbar: Record<string, unknown>; footer: Record<string, unknown> | null }
-export interface ChromeUrls { theme: string; navbar: string; footer: string; shipped: string }
-
-export const DEFAULT_BASE: string;
-export function scriptUrls(opts?: { base?: string }): ChromeUrls;
-export function chromeConfig(opts?: ChromeOptions): ChromeConfig;
-export function chromeTags(opts?: ChromeOptions): { head: string; bodyStart: string; bodyEnd: string; config: ChromeConfig; urls: ChromeUrls };
-export function mountChrome(opts?: ChromeOptions): Promise<{ navbar: unknown | null; footer: unknown | null }>;
new file mode 100644
new file mode 100644
@@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
+// openvibe-sdk/chrome — the OpenVibe network's shared navbar, footer, "shipped" views and theme loader for any app.
+
+export interface ChromeLink { label: string; href: string; icon?: string; active?: boolean }
+export interface ChromeMenuItem { label: string; href?: string; icon?: string; onClick?: () => void; danger?: boolean; external?: boolean }
+export interface ChromeFooterConfig {
+ links?: Array<{ heading: string; items: Array<{ name: string; url: string }> }>;
+ variant?: 'full' | 'compact';
+ /** This app's update log; default: https://openvibe.network/updates?site=<host>. */
+ updates?: string;
+ mount?: string;
+ [key: string]: unknown;
+}
+export interface ChromeOptions {
+ /** Your app's id (brand and analytics). */
+ service?: string;
+ brand?: { name?: string; sub?: string; tld?: string; icon?: string; variant?: string };
+ links?: ChromeLink[];
+ menu?: { before?: ChromeMenuItem[]; after?: ChromeMenuItem[] };
+ /** Same-origin endpoint answering { user } for your server session. */
+ sessionUrl?: string;
+ /** Sign-in URL; {url} = full return URL, {path} = its local path. */
+ loginUrl?: string;
+ /** Sign-out URL, so your server session ends too; {url} / {path} as for loginUrl. */
+ logoutUrl?: string;
+ silentLogin?: string;
+ /** Footer options, or false for no footer. */
+ footer?: ChromeFooterConfig | false;
+ /** false: no "shipped X ago" line in the footer. */
+ shipped?: boolean;
+ /** false: do not load the shared theme loader. */
+ theme?: boolean;
+ /** Where the shared chrome is served (default https://openvibe.network). */
+ base?: string;
+ /** Extra navbar options passed through to OpenVibeNavbar.init. */
+ navbar?: Record<string, unknown>;
+ document?: unknown;
+ window?: unknown;
+}
+export interface ChromeConfig { navbar: Record<string, unknown>; footer: Record<string, unknown> | null }
+export interface ChromeUrls { theme: string; navbar: string; footer: string; shipped: string }
+
+export const DEFAULT_BASE: string;
+export function scriptUrls(opts?: { base?: string }): ChromeUrls;
+export function chromeConfig(opts?: ChromeOptions): ChromeConfig;
+export function chromeTags(opts?: ChromeOptions): { head: string; bodyStart: string; bodyEnd: string; config: ChromeConfig; urls: ChromeUrls };
+export function mountChrome(opts?: ChromeOptions): Promise<{ navbar: unknown | null; footer: unknown | null }>;