openvibers/openvibe.shared Commits
4.173.0: the wallet works on sites with their own cookie session; the sheet goes by its hash (#391)
Changes
@@@ -1,5 +1,17 @@# Changelog++## 4.173.0 — 2026-11-09++++- **The wallet works on sites that keep the access cookie from page scripts** (`navbar.js`, `ov-wallet.js`). 4.170's++ chips and sheet sent only a Bearer token. The sites on `openvibe-sdk/sso` with `httpOnlyAccess` (Actor, Bench, Casino,++ Codes, Connect, Design, Exchange, Vision) hand the navbar none, so their chips sat at "…" and Wallet did not open.++ With no token, the chips and the sheet now ask Network's relay with the person's Network session cookie++ (`credentials: 'include'`; Network #724 turns it into the Bearer, only for allowed origins). A wallet that cannot be++ read hides the chips instead of leaving "…". `OpenVibeWallet.open` takes `cookieSession: true`. Sites whose navbar++ reads `ov_token` are unchanged.++- **The wallet sheet goes by its content hash** (`scripts/sibling-revs.js`): `ov-wallet.js` and `ov-wallet.css` were++ loaded without `?v=` since 4.170, so a fix reached phones up to a day late.++## 4.172.0 — 2026-11-09- **Notices read as what happened** (`notification-ui.js`, the bell, its toasts and the Dock's Activity tab): an@@@ -2,7 +2,7 @@> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.--**Status:** alpha, v4.172.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.173.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.172.0",++ "version": "4.173.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",@@@ -572,7 +572,7 @@const NAV_ICONS_REV = '1091daee9c7e'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)// END generated// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.-- const SIBLING_REV = { 'ov-mark.js': 'd7e87def8fff', 'ov-glyphs.js': '6f8c8dd2a10d', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2d3a47d3535c', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '20f4ef41d127', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '8e55bfe93238', 'ov-share.css': 'cbd18e6ce991', 'search.js': '8d00547b413c', 'dock.js': '7ff462f31951', 'presence.js': '1b675c37d888', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', 'notification-ui.js': 'e06f1388b696', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': 'd18308ab8a63', 'ov-home.js': 'fddb428eb75a', 'nav-slots.js': 'a5af310d62e0' };++ const SIBLING_REV = { 'ov-mark.js': 'd7e87def8fff', 'ov-glyphs.js': '6f8c8dd2a10d', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2d3a47d3535c', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '20f4ef41d127', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '8e55bfe93238', 'ov-share.css': 'cbd18e6ce991', 'search.js': '8d00547b413c', 'dock.js': '7ff462f31951', 'presence.js': '1b675c37d888', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', 'notification-ui.js': 'e06f1388b696', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': 'd18308ab8a63', 'ov-home.js': 'fddb428eb75a', 'nav-slots.js': 'a5af310d62e0', 'ov-wallet.js': '5c4d7fdb448d', 'ov-wallet.css': 'e46f653fa59e' };// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.@@@ -2043,8 +2043,9 @@if (!_config.wallet) return;const u = _config.user;let chips = (_config.chips || []).filter((c) => !(c && (c.__wallet || ['coins', 'vibes', 'balance'].includes(c.id))));-- if (u && !u.is_anon) {-- const data = _walletFor === walletKey(u) ? _walletData : null;++ // false: unreadable, nothing kept: no chips, never a "…" that stays.++ const data = u && !u.is_anon && _walletFor === walletKey(u) ? _walletData : null;++ if (u && !u.is_anon && data !== false) {chips = walletOrder(data).map((id) => Object.assign({}, WALLET_CHIPS.find((w) => w.id === id), {__wallet: true, value: data ? walletAmount(data.balances[id]) : '…', hidden: false, onClick: openWallet,})).concat(chips);@@@ -2052,7 +2053,7 @@_config.chips = chips;}async function openWallet() {-- if (!_config.token) return;++ if (!_config.user || _config.user.is_anon) return;if (!root.OpenVibeWallet) {if (!_walletScript) _walletScript = new Promise((resolve, reject) => {const css = document.createElement('link'); css.rel = 'stylesheet'; css.href = siblingV('ov-wallet.css'); document.head.appendChild(css);@@@ -2061,13 +2062,13 @@});try { await _walletScript; } catch { if (_walletScript === null) root.location.assign('https://openvibe.network/wallet'); return; }}-- return root.OpenVibeWallet.open({ getToken: () => _config.token, frameBase: _config.frameBase || 'https://openvibe.network/api/frame',++ return root.OpenVibeWallet.open({ getToken: () => _config.token || null, cookieSession: !_config.token, frameBase: _config.frameBase || 'https://openvibe.network/api/frame',onWallet: (data) => { if (!_config.user) return; _walletFor = walletKey(_config.user); _walletData = data; _walletAt = Date.now(); if (_navEl) render(); } });}function loadWalletChips() {const u = _config.user;-- if (!_config.wallet || !u || u.is_anon || !_config.token || typeof fetch !== 'function') return;-- const key = walletKey(u), token = _config.token;++ if (!_config.wallet || !u || u.is_anon || typeof fetch !== 'function') return;++ const key = walletKey(u), token = _config.token || null;if (_walletFor === key && Date.now() - _walletAt < 60000) return;if (_walletFor !== key) _walletData = null;_walletFor = key; _walletAt = Date.now();@@@ -2075,9 +2076,13 @@_walletWatch = true;document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { loadWalletChips(); loadUnread(); } });}-- fetch(`${(_config.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '')}/wallet/api/v1/me/wallet`, { headers: { Authorization: `Bearer ${token}` }, credentials: 'omit' })-- .then(async (r) => { if (!r.ok) return; const data = await r.json(); if (_config.user !== u || _config.token !== token) return;-- _walletData = data; if (_navEl) render(); }).catch(() => {});++ // No token (httpOnlyAccess sites): the Network cookie reads it. Unreadable with nothing kept hides the chips.++ const same = () => _config.user === u && (_config.token || null) === token;++ const failed = () => { if (!same() || _walletData) return; _walletData = false; if (_navEl) render(); };++ fetch(`${(_config.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '')}/wallet/api/v1/me/wallet`,++ token ? { headers: { Authorization: `Bearer ${token}` }, credentials: 'omit' } : { credentials: 'include' })++ .then(async (r) => { if (!r.ok) return failed(); const data = await r.json(); if (!same()) return;++ _walletData = data; if (_navEl) render(); }).catch(failed);}function render() {@@@ -25,8 +25,10 @@}async function open(options) {close();-- const token = options.getToken ? options.getToken() : options.token;-- if (!token) throw new Error('Sign in to open your wallet');++ const token = (options.getToken ? options.getToken() : options.token) || null;++ // cookieSession: no token for the page (httpOnlyAccess sites): ask with the person's Network cookie instead.++ const cookie = !token && options.cookieSession === true;++ if (!token && !cookie) throw new Error('Sign in to open your wallet');// Every site's connect-src allows openvibe.network and refuses billing./inventory.openvibe.network, so the// sheet asks Network's relays (/api/frame/wallet/…); Network forwards the bearer and never a cookie.const walletApi = `${(options.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '')}/wallet`;@@@ -55,12 +57,12 @@} };active = session;const identity = () => {-- if (disposed || (options.getToken && options.getToken() !== token)) { session.close(); throw new Error('Your account changed. Open Wallet again.'); }++ if (disposed || (options.getToken && (options.getToken() || null) !== token)) { session.close(); throw new Error('Your account changed. Open Wallet again.'); }};async function request(path, method = 'GET', payload, publicRead = false) {identity();-- const r = await fetch(walletApi + path, { method, credentials: 'omit', signal: controller.signal,-- headers: { ...(publicRead ? {} : { Authorization: `Bearer ${token}` }), ...(payload ? { 'Content-Type': 'application/json' } : {}) },++ const r = await fetch(walletApi + path, { method, credentials: cookie && !publicRead ? 'include' : 'omit', signal: controller.signal,++ headers: { ...(publicRead || !token ? {} : { Authorization: `Bearer ${token}` }), ...(payload ? { 'Content-Type': 'application/json' } : {}) },...(payload ? { body: JSON.stringify(payload) } : {}) });const value = await r.json(); identity();if (!r.ok) throw new Error(value.detail || 'This is temporarily unavailable. Please try again.');@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "4.172.0",++ "version": "4.173.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "4.172.0",++ "version": "4.173.0","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.172.0",++ "version": "4.173.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -13,7 +13,9 @@ const ROOT = path.join(__dirname, '..');// went out without ?v=, so a release reached phones up to a day late (max-age=300, stale-while-revalidate=86400): the// owner still saw the old menu hours after the new one shipped (11-07).const FILES = ['ov-mark.js', 'ov-glyphs.js', 'ov-brand.woff2', 'ov-look.js', 'ov-fx.js', 'ov-labels.js', 'ov-forms.js', 'ov-forms.css', 'ov-share.js', 'ov-share.css',-- 'search.js', 'dock.js', 'presence.js', 'panels.js', 'surfaces.js', 'notification-ui.js', 'history.js', 'site-switcher.js', 'appbar.js', 'release-watch.js', 'ov-icons.js', 'sso-client.js', 'overlay.js', 'ov-home.js', 'nav-slots.js'];++ 'search.js', 'dock.js', 'presence.js', 'panels.js', 'surfaces.js', 'notification-ui.js', 'history.js', 'site-switcher.js', 'appbar.js', 'release-watch.js', 'ov-icons.js', 'sso-client.js', 'overlay.js', 'ov-home.js', 'nav-slots.js',++ // 4.173.0: the wallet sheet (4.170.0) went out without ?v=, so a fix to it reached phones up to a day late.++ 'ov-wallet.js', 'ov-wallet.css'];// The files dock.js and notification-ui.js load from beside themselves: their own tables, in their own files.const OWN = { 'dock.js': { name: 'DOCK_REV', files: ['dock-vibe.js', 'dock-runs.js', 'ai-marks-client.js', 'action-center.js', 'dock-timeline.js', 'dock-actor.js', 'dock-agent.js', 'dock-timer.js', 'dock-share.js', 'dock-card.js'] },'notification-ui.js': { name: 'NOTIFY_REV', files: ['notification-live.js'] },@@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');const ROOT = path.join(__dirname, '..');const NAVBAR_PATH = path.join(ROOT, 'navbar.js');const ICONS = require('../nav-icons');--const NAVBAR_BROTLI_BUDGET = 48.5 * 1024; // 48.2 KB with the same-site prerender rule, cross-site prefetch and the file-cache worker registration (4.165.0, 2026-11-09); 47.6 KB with the six glyph styles' refined paths and the sheets' surface loader (4.162.0, 2026-11-09); 47.1 KB with the shared pop motion every site's menus use and the account menu's Wallet and Profile (4.161.0, 2026-11-09); 46.6 KB with the keyboard watcher every site needs and the slots' loader (4.160.0, 2026-11-09; the slots themselves are a sibling, nav-slots.js); 45.0 KB with the floating cards' loader and link warming (4.152.0, 2026-11-08; the cards themselves are a sibling, overlay.js); 44.3 KB with the phone's menu button drawn as the orb (4.148.0, 2026-11-08); 43.5 KB with the menu warmed at idle and on intent (4.137.0); 43.2 KB with every sibling's ?v= content hash (4.137.0, 2026-11-07: the owner's menu was a day stale without them); 42.8 KB keeping the menu's Frame fields (4.132.0, 2026-11-07); 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)++const NAVBAR_BROTLI_BUDGET = 48.75 * 1024; // 48.6 KB with the wallet's cookie mode and its two content hashes (4.173.0, 2026-11-09); 48.2 KB with the same-site prerender rule, cross-site prefetch and the file-cache worker registration (4.165.0, 2026-11-09); 47.6 KB with the six glyph styles' refined paths and the sheets' surface loader (4.162.0, 2026-11-09); 47.1 KB with the shared pop motion every site's menus use and the account menu's Wallet and Profile (4.161.0, 2026-11-09); 46.6 KB with the keyboard watcher every site needs and the slots' loader (4.160.0, 2026-11-09; the slots themselves are a sibling, nav-slots.js); 45.0 KB with the floating cards' loader and link warming (4.152.0, 2026-11-08; the cards themselves are a sibling, overlay.js); 44.3 KB with the phone's menu button drawn as the orb (4.148.0, 2026-11-08); 43.5 KB with the menu warmed at idle and on intent (4.137.0); 43.2 KB with every sibling's ?v= content hash (4.137.0, 2026-11-07: the owner's menu was a day stale without them); 42.8 KB keeping the menu's Frame fields (4.132.0, 2026-11-07); 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)// ── size budget and generated block ──────────────────────────{@@@ -22,7 +22,10 @@ const PAGE = (signedIn) => `<!doctype html><html lang="en"><head><meta charset="<body><div id="navbar-mount"></div><main><h1>Games</h1></main><script>localStorage.setItem('ov-wallet:42', JSON.stringify({ coins: 161000, vibes: 400 }));-- window.fetch = async () => new Response('{}', { status: 404 });++ // The signed-in bar's balances come from the wallet read (4.173: an unreadable wallet shows no chips at all).++ window.fetch = async (u) => (String(u).endsWith('/api/v1/me/wallet')++ ? new Response(JSON.stringify({ balances: { coins: { amount: '161000', scale: 0, currency: 'opencoins' }, vibes: { amount: '400', scale: 0, currency: 'vibes-bits' }, balance: { amount: '0', scale: 9, currency: 'USD' } } }), { status: 200, headers: { 'content-type': 'application/json' } })++ : new Response('{}', { status: 404 }));</script><script src="/navbar.js"></script><script>@@@ -34,13 +34,15 @@ function load(store, answers) {global.localStorage = { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: (k) => { delete store[k]; } };global.sessionStorage = { getItem: () => null, setItem() {}, removeItem() {} };const calls = [];-- global.fetch = async (url) => {++ const inits = [];++ global.fetch = async (url, init = {}) => {calls.push(url);++ inits.push(init);const a = url.endsWith('/api/v1/me/wallet') ? answers.wallet : { status: 404, body: {} };return { ok: a.status >= 200 && a.status < 300, status: a.status, json: async () => a.body };};delete require.cache[require.resolve(NAVBAR_PATH)];-- return { navbar: require(NAVBAR_PATH), navs, calls };++ return { navbar: require(NAVBAR_PATH), navs, calls, inits };}const settle = () => new Promise((r) => setTimeout(r, 20));const wallet = (calls) => calls.filter((u) => /\/api\/(coins\/me|me\/vibes)(\?|$)/.test(u));@@@ -64,5 +66,25 @@ const base = { service: 'games', auth: 'external', apiBase: 'https://openvibe.neassert.strictEqual(navbar._wallet.walletOrder(data)[0], 'balance');navbar.setUser(null, null); await settle();assert(!navs[navs.length - 1].innerHTML.includes('data-chip-id="coins"'));-- console.log('wallet chips: one Billing read, all zeros supported, Balance promoted for AI users, signout clears chips');++++ // A site that signs people in with its own cookie gives the navbar no token: the chips read the wallet with the++ // person's Network cookie (credentials: 'include', no Authorization), and a wallet that cannot be read hides them:++ // never a "…" that stays (2026-11-09: 4.170's chips sat at "…" on every such site).++ {++ const cookieSite = load({}, { wallet: { status: 200, body: data } });++ cookieSite.navbar.init({ ...base, token: null });++ await settle();++ const i = cookieSite.calls.findIndex((u) => u.endsWith('/api/v1/me/wallet'));++ assert(i >= 0, 'asked without a token');++ assert.strictEqual(cookieSite.inits[i].credentials, 'include', "the person's Network cookie goes along");++ assert(!(cookieSite.inits[i].headers && cookieSite.inits[i].headers.Authorization), 'no Authorization header');++ assert.match(cookieSite.navs[cookieSite.navs.length - 1].innerHTML, /data-chip-id="(coins|vibes|balance)"/, "the chips show");++ const signedOut = load({}, { wallet: { status: 401, body: { error: 'sign in' } } });++ signedOut.navbar.init({ ...base, token: null });++ await settle();++ const last = signedOut.navs[signedOut.navs.length - 1].innerHTML;++ assert(!/data-chip-id="(coins|vibes|balance)"/.test(last), 'no chips when the wallet cannot be read');++ assert(!last.includes('…'), 'and no "…" left behind');++ }++ console.log('wallet chips: one Billing read, all zeros supported, Balance promoted for AI users, signout clears chips, cookie sites read by cookie');})().catch((e) => { console.error(e); process.exit(1); });
@@ -1,5 +1,17 @@
@@ -1,5 +1,17 @@
# Changelog
# Changelog
+## 4.173.0 — 2026-11-09
+
+- **The wallet works on sites that keep the access cookie from page scripts** (`navbar.js`, `ov-wallet.js`). 4.170's
+ chips and sheet sent only a Bearer token. The sites on `openvibe-sdk/sso` with `httpOnlyAccess` (Actor, Bench, Casino,
+ Codes, Connect, Design, Exchange, Vision) hand the navbar none, so their chips sat at "…" and Wallet did not open.
+ With no token, the chips and the sheet now ask Network's relay with the person's Network session cookie
+ (`credentials: 'include'`; Network #724 turns it into the Bearer, only for allowed origins). A wallet that cannot be
+ read hides the chips instead of leaving "…". `OpenVibeWallet.open` takes `cookieSession: true`. Sites whose navbar
+ reads `ov_token` are unchanged.
+- **The wallet sheet goes by its content hash** (`scripts/sibling-revs.js`): `ov-wallet.js` and `ov-wallet.css` were
+ loaded without `?v=` since 4.170, so a fix reached phones up to a day late.
+
## 4.172.0 — 2026-11-09
## 4.172.0 — 2026-11-09
- **Notices read as what happened** (`notification-ui.js`, the bell, its toasts and the Dock's Activity tab): an
- **Notices read as what happened** (`notification-ui.js`, the bell, its toasts and the Dock's Activity tab): an
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** alpha, v4.172.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.173.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.172.0",
+ "version": "4.173.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@ -572,7 +572,7 @@
@@ -572,7 +572,7 @@
const NAV_ICONS_REV = '1091daee9c7e'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
const NAV_ICONS_REV = '1091daee9c7e'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
// END generated
// END generated
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
- const SIBLING_REV = { 'ov-mark.js': 'd7e87def8fff', 'ov-glyphs.js': '6f8c8dd2a10d', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2d3a47d3535c', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '20f4ef41d127', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '8e55bfe93238', 'ov-share.css': 'cbd18e6ce991', 'search.js': '8d00547b413c', 'dock.js': '7ff462f31951', 'presence.js': '1b675c37d888', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', 'notification-ui.js': 'e06f1388b696', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': 'd18308ab8a63', 'ov-home.js': 'fddb428eb75a', 'nav-slots.js': 'a5af310d62e0' };
+ const SIBLING_REV = { 'ov-mark.js': 'd7e87def8fff', 'ov-glyphs.js': '6f8c8dd2a10d', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2d3a47d3535c', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '20f4ef41d127', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '8e55bfe93238', 'ov-share.css': 'cbd18e6ce991', 'search.js': '8d00547b413c', 'dock.js': '7ff462f31951', 'presence.js': '1b675c37d888', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', 'notification-ui.js': 'e06f1388b696', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': 'd18308ab8a63', 'ov-home.js': 'fddb428eb75a', 'nav-slots.js': 'a5af310d62e0', 'ov-wallet.js': '5c4d7fdb448d', 'ov-wallet.css': 'e46f653fa59e' };
// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest
// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest
// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their
// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
@@ -2043,8 +2043,9 @@
@@ -2043,8 +2043,9 @@
if (!_config.wallet) return;
if (!_config.wallet) return;
const u = _config.user;
const u = _config.user;
let chips = (_config.chips || []).filter((c) => !(c && (c.__wallet || ['coins', 'vibes', 'balance'].includes(c.id))));
let chips = (_config.chips || []).filter((c) => !(c && (c.__wallet || ['coins', 'vibes', 'balance'].includes(c.id))));
- if (u && !u.is_anon) {
- const data = _walletFor === walletKey(u) ? _walletData : null;
+ // false: unreadable, nothing kept: no chips, never a "…" that stays.
+ const data = u && !u.is_anon && _walletFor === walletKey(u) ? _walletData : null;
+ if (u && !u.is_anon && data !== false) {
chips = walletOrder(data).map((id) => Object.assign({}, WALLET_CHIPS.find((w) => w.id === id), {
chips = walletOrder(data).map((id) => Object.assign({}, WALLET_CHIPS.find((w) => w.id === id), {
__wallet: true, value: data ? walletAmount(data.balances[id]) : '…', hidden: false, onClick: openWallet,
__wallet: true, value: data ? walletAmount(data.balances[id]) : '…', hidden: false, onClick: openWallet,
})).concat(chips);
})).concat(chips);
@@ -2052,7 +2053,7 @@
@@ -2052,7 +2053,7 @@
_config.chips = chips;
_config.chips = chips;
}
}
async function openWallet() {
async function openWallet() {
- if (!_config.token) return;
+ if (!_config.user || _config.user.is_anon) return;
if (!root.OpenVibeWallet) {
if (!root.OpenVibeWallet) {
if (!_walletScript) _walletScript = new Promise((resolve, reject) => {
if (!_walletScript) _walletScript = new Promise((resolve, reject) => {
const css = document.createElement('link'); css.rel = 'stylesheet'; css.href = siblingV('ov-wallet.css'); document.head.appendChild(css);
const css = document.createElement('link'); css.rel = 'stylesheet'; css.href = siblingV('ov-wallet.css'); document.head.appendChild(css);
@@ -2061,13 +2062,13 @@
@@ -2061,13 +2062,13 @@
});
});
try { await _walletScript; } catch { if (_walletScript === null) root.location.assign('https://openvibe.network/wallet'); return; }
try { await _walletScript; } catch { if (_walletScript === null) root.location.assign('https://openvibe.network/wallet'); return; }
}
}
- return root.OpenVibeWallet.open({ getToken: () => _config.token, frameBase: _config.frameBase || 'https://openvibe.network/api/frame',
+ return root.OpenVibeWallet.open({ getToken: () => _config.token || null, cookieSession: !_config.token, frameBase: _config.frameBase || 'https://openvibe.network/api/frame',
onWallet: (data) => { if (!_config.user) return; _walletFor = walletKey(_config.user); _walletData = data; _walletAt = Date.now(); if (_navEl) render(); } });
onWallet: (data) => { if (!_config.user) return; _walletFor = walletKey(_config.user); _walletData = data; _walletAt = Date.now(); if (_navEl) render(); } });
}
}
function loadWalletChips() {
function loadWalletChips() {
const u = _config.user;
const u = _config.user;
- if (!_config.wallet || !u || u.is_anon || !_config.token || typeof fetch !== 'function') return;
- const key = walletKey(u), token = _config.token;
+ if (!_config.wallet || !u || u.is_anon || typeof fetch !== 'function') return;
+ const key = walletKey(u), token = _config.token || null;
if (_walletFor === key && Date.now() - _walletAt < 60000) return;
if (_walletFor === key && Date.now() - _walletAt < 60000) return;
if (_walletFor !== key) _walletData = null;
if (_walletFor !== key) _walletData = null;
_walletFor = key; _walletAt = Date.now();
_walletFor = key; _walletAt = Date.now();
@@ -2075,9 +2076,13 @@
@@ -2075,9 +2076,13 @@
_walletWatch = true;
_walletWatch = true;
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { loadWalletChips(); loadUnread(); } });
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { loadWalletChips(); loadUnread(); } });
}
}
- fetch(`${(_config.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '')}/wallet/api/v1/me/wallet`, { headers: { Authorization: `Bearer ${token}` }, credentials: 'omit' })
- .then(async (r) => { if (!r.ok) return; const data = await r.json(); if (_config.user !== u || _config.token !== token) return;
- _walletData = data; if (_navEl) render(); }).catch(() => {});
+ // No token (httpOnlyAccess sites): the Network cookie reads it. Unreadable with nothing kept hides the chips.
+ const same = () => _config.user === u && (_config.token || null) === token;
+ const failed = () => { if (!same() || _walletData) return; _walletData = false; if (_navEl) render(); };
+ fetch(`${(_config.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '')}/wallet/api/v1/me/wallet`,
+ token ? { headers: { Authorization: `Bearer ${token}` }, credentials: 'omit' } : { credentials: 'include' })
+ .then(async (r) => { if (!r.ok) return failed(); const data = await r.json(); if (!same()) return;
+ _walletData = data; if (_navEl) render(); }).catch(failed);
}
}
function render() {
function render() {
@@ -25,8 +25,10 @@
@@ -25,8 +25,10 @@
}
}
async function open(options) {
async function open(options) {
close();
close();
- const token = options.getToken ? options.getToken() : options.token;
- if (!token) throw new Error('Sign in to open your wallet');
+ const token = (options.getToken ? options.getToken() : options.token) || null;
+ // cookieSession: no token for the page (httpOnlyAccess sites): ask with the person's Network cookie instead.
+ const cookie = !token && options.cookieSession === true;
+ if (!token && !cookie) throw new Error('Sign in to open your wallet');
// Every site's connect-src allows openvibe.network and refuses billing./inventory.openvibe.network, so the
// Every site's connect-src allows openvibe.network and refuses billing./inventory.openvibe.network, so the
// sheet asks Network's relays (/api/frame/wallet/…); Network forwards the bearer and never a cookie.
// sheet asks Network's relays (/api/frame/wallet/…); Network forwards the bearer and never a cookie.
const walletApi = `${(options.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '')}/wallet`;
const walletApi = `${(options.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '')}/wallet`;
@@ -55,12 +57,12 @@
@@ -55,12 +57,12 @@
} };
} };
active = session;
active = session;
const identity = () => {
const identity = () => {
- if (disposed || (options.getToken && options.getToken() !== token)) { session.close(); throw new Error('Your account changed. Open Wallet again.'); }
+ if (disposed || (options.getToken && (options.getToken() || null) !== token)) { session.close(); throw new Error('Your account changed. Open Wallet again.'); }
};
};
async function request(path, method = 'GET', payload, publicRead = false) {
async function request(path, method = 'GET', payload, publicRead = false) {
identity();
identity();
- const r = await fetch(walletApi + path, { method, credentials: 'omit', signal: controller.signal,
- headers: { ...(publicRead ? {} : { Authorization: `Bearer ${token}` }), ...(payload ? { 'Content-Type': 'application/json' } : {}) },
+ const r = await fetch(walletApi + path, { method, credentials: cookie && !publicRead ? 'include' : 'omit', signal: controller.signal,
+ headers: { ...(publicRead || !token ? {} : { Authorization: `Bearer ${token}` }), ...(payload ? { 'Content-Type': 'application/json' } : {}) },
...(payload ? { body: JSON.stringify(payload) } : {}) });
...(payload ? { body: JSON.stringify(payload) } : {}) });
const value = await r.json(); identity();
const value = await r.json(); identity();
if (!r.ok) throw new Error(value.detail || 'This is temporarily unavailable. Please try again.');
if (!r.ok) throw new Error(value.detail || 'This is temporarily unavailable. Please try again.');
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.172.0",
+ "version": "4.173.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.172.0",
+ "version": "4.173.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.172.0",
+ "version": "4.173.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -13,7 +13,9 @@ const ROOT = path.join(__dirname, '..');
@@ -13,7 +13,9 @@ const ROOT = path.join(__dirname, '..');
// went out without ?v=, so a release reached phones up to a day late (max-age=300, stale-while-revalidate=86400): the
// went out without ?v=, so a release reached phones up to a day late (max-age=300, stale-while-revalidate=86400): the
// owner still saw the old menu hours after the new one shipped (11-07).
// owner still saw the old menu hours after the new one shipped (11-07).
const FILES = ['ov-mark.js', 'ov-glyphs.js', 'ov-brand.woff2', 'ov-look.js', 'ov-fx.js', 'ov-labels.js', 'ov-forms.js', 'ov-forms.css', 'ov-share.js', 'ov-share.css',
const FILES = ['ov-mark.js', 'ov-glyphs.js', 'ov-brand.woff2', 'ov-look.js', 'ov-fx.js', 'ov-labels.js', 'ov-forms.js', 'ov-forms.css', 'ov-share.js', 'ov-share.css',
- 'search.js', 'dock.js', 'presence.js', 'panels.js', 'surfaces.js', 'notification-ui.js', 'history.js', 'site-switcher.js', 'appbar.js', 'release-watch.js', 'ov-icons.js', 'sso-client.js', 'overlay.js', 'ov-home.js', 'nav-slots.js'];
+ 'search.js', 'dock.js', 'presence.js', 'panels.js', 'surfaces.js', 'notification-ui.js', 'history.js', 'site-switcher.js', 'appbar.js', 'release-watch.js', 'ov-icons.js', 'sso-client.js', 'overlay.js', 'ov-home.js', 'nav-slots.js',
+ // 4.173.0: the wallet sheet (4.170.0) went out without ?v=, so a fix to it reached phones up to a day late.
+ 'ov-wallet.js', 'ov-wallet.css'];
// The files dock.js and notification-ui.js load from beside themselves: their own tables, in their own files.
// The files dock.js and notification-ui.js load from beside themselves: their own tables, in their own files.
const OWN = { 'dock.js': { name: 'DOCK_REV', files: ['dock-vibe.js', 'dock-runs.js', 'ai-marks-client.js', 'action-center.js', 'dock-timeline.js', 'dock-actor.js', 'dock-agent.js', 'dock-timer.js', 'dock-share.js', 'dock-card.js'] },
const OWN = { 'dock.js': { name: 'DOCK_REV', files: ['dock-vibe.js', 'dock-runs.js', 'ai-marks-client.js', 'action-center.js', 'dock-timeline.js', 'dock-actor.js', 'dock-agent.js', 'dock-timer.js', 'dock-share.js', 'dock-card.js'] },
'notification-ui.js': { name: 'NOTIFY_REV', files: ['notification-live.js'] },
'notification-ui.js': { name: 'NOTIFY_REV', files: ['notification-live.js'] },
@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');
@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');
const ROOT = path.join(__dirname, '..');
const ROOT = path.join(__dirname, '..');
const NAVBAR_PATH = path.join(ROOT, 'navbar.js');
const NAVBAR_PATH = path.join(ROOT, 'navbar.js');
const ICONS = require('../nav-icons');
const ICONS = require('../nav-icons');
-const NAVBAR_BROTLI_BUDGET = 48.5 * 1024; // 48.2 KB with the same-site prerender rule, cross-site prefetch and the file-cache worker registration (4.165.0, 2026-11-09); 47.6 KB with the six glyph styles' refined paths and the sheets' surface loader (4.162.0, 2026-11-09); 47.1 KB with the shared pop motion every site's menus use and the account menu's Wallet and Profile (4.161.0, 2026-11-09); 46.6 KB with the keyboard watcher every site needs and the slots' loader (4.160.0, 2026-11-09; the slots themselves are a sibling, nav-slots.js); 45.0 KB with the floating cards' loader and link warming (4.152.0, 2026-11-08; the cards themselves are a sibling, overlay.js); 44.3 KB with the phone's menu button drawn as the orb (4.148.0, 2026-11-08); 43.5 KB with the menu warmed at idle and on intent (4.137.0); 43.2 KB with every sibling's ?v= content hash (4.137.0, 2026-11-07: the owner's menu was a day stale without them); 42.8 KB keeping the menu's Frame fields (4.132.0, 2026-11-07); 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)
+const NAVBAR_BROTLI_BUDGET = 48.75 * 1024; // 48.6 KB with the wallet's cookie mode and its two content hashes (4.173.0, 2026-11-09); 48.2 KB with the same-site prerender rule, cross-site prefetch and the file-cache worker registration (4.165.0, 2026-11-09); 47.6 KB with the six glyph styles' refined paths and the sheets' surface loader (4.162.0, 2026-11-09); 47.1 KB with the shared pop motion every site's menus use and the account menu's Wallet and Profile (4.161.0, 2026-11-09); 46.6 KB with the keyboard watcher every site needs and the slots' loader (4.160.0, 2026-11-09; the slots themselves are a sibling, nav-slots.js); 45.0 KB with the floating cards' loader and link warming (4.152.0, 2026-11-08; the cards themselves are a sibling, overlay.js); 44.3 KB with the phone's menu button drawn as the orb (4.148.0, 2026-11-08); 43.5 KB with the menu warmed at idle and on intent (4.137.0); 43.2 KB with every sibling's ?v= content hash (4.137.0, 2026-11-07: the owner's menu was a day stale without them); 42.8 KB keeping the menu's Frame fields (4.132.0, 2026-11-07); 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)
// ── size budget and generated block ──────────────────────────
// ── size budget and generated block ──────────────────────────
{
{
@@ -22,7 +22,10 @@ const PAGE = (signedIn) => `<!doctype html><html lang="en"><head><meta charset="
@@ -22,7 +22,10 @@ const PAGE = (signedIn) => `<!doctype html><html lang="en"><head><meta charset="
<body><div id="navbar-mount"></div><main><h1>Games</h1></main>
<body><div id="navbar-mount"></div><main><h1>Games</h1></main>
<script>
<script>
localStorage.setItem('ov-wallet:42', JSON.stringify({ coins: 161000, vibes: 400 }));
localStorage.setItem('ov-wallet:42', JSON.stringify({ coins: 161000, vibes: 400 }));
- window.fetch = async () => new Response('{}', { status: 404 });
+ // The signed-in bar's balances come from the wallet read (4.173: an unreadable wallet shows no chips at all).
+ window.fetch = async (u) => (String(u).endsWith('/api/v1/me/wallet')
+ ? new Response(JSON.stringify({ balances: { coins: { amount: '161000', scale: 0, currency: 'opencoins' }, vibes: { amount: '400', scale: 0, currency: 'vibes-bits' }, balance: { amount: '0', scale: 9, currency: 'USD' } } }), { status: 200, headers: { 'content-type': 'application/json' } })
+ : new Response('{}', { status: 404 }));
</script>
</script>
<script src="/navbar.js"></script>
<script src="/navbar.js"></script>
<script>
<script>
@@ -34,13 +34,15 @@ function load(store, answers) {
@@ -34,13 +34,15 @@ function load(store, answers) {
global.localStorage = { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: (k) => { delete store[k]; } };
global.localStorage = { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: (k) => { delete store[k]; } };
global.sessionStorage = { getItem: () => null, setItem() {}, removeItem() {} };
global.sessionStorage = { getItem: () => null, setItem() {}, removeItem() {} };
const calls = [];
const calls = [];
- global.fetch = async (url) => {
+ const inits = [];
+ global.fetch = async (url, init = {}) => {
calls.push(url);
calls.push(url);
+ inits.push(init);
const a = url.endsWith('/api/v1/me/wallet') ? answers.wallet : { status: 404, body: {} };
const a = url.endsWith('/api/v1/me/wallet') ? answers.wallet : { status: 404, body: {} };
return { ok: a.status >= 200 && a.status < 300, status: a.status, json: async () => a.body };
return { ok: a.status >= 200 && a.status < 300, status: a.status, json: async () => a.body };
};
};
delete require.cache[require.resolve(NAVBAR_PATH)];
delete require.cache[require.resolve(NAVBAR_PATH)];
- return { navbar: require(NAVBAR_PATH), navs, calls };
+ return { navbar: require(NAVBAR_PATH), navs, calls, inits };
}
}
const settle = () => new Promise((r) => setTimeout(r, 20));
const settle = () => new Promise((r) => setTimeout(r, 20));
const wallet = (calls) => calls.filter((u) => /\/api\/(coins\/me|me\/vibes)(\?|$)/.test(u));
const wallet = (calls) => calls.filter((u) => /\/api\/(coins\/me|me\/vibes)(\?|$)/.test(u));
@@ -64,5 +66,25 @@ const base = { service: 'games', auth: 'external', apiBase: 'https://openvibe.ne
@@ -64,5 +66,25 @@ const base = { service: 'games', auth: 'external', apiBase: 'https://openvibe.ne
assert.strictEqual(navbar._wallet.walletOrder(data)[0], 'balance');
assert.strictEqual(navbar._wallet.walletOrder(data)[0], 'balance');
navbar.setUser(null, null); await settle();
navbar.setUser(null, null); await settle();
assert(!navs[navs.length - 1].innerHTML.includes('data-chip-id="coins"'));
assert(!navs[navs.length - 1].innerHTML.includes('data-chip-id="coins"'));
- console.log('wallet chips: one Billing read, all zeros supported, Balance promoted for AI users, signout clears chips');
+
+ // A site that signs people in with its own cookie gives the navbar no token: the chips read the wallet with the
+ // person's Network cookie (credentials: 'include', no Authorization), and a wallet that cannot be read hides them:
+ // never a "…" that stays (2026-11-09: 4.170's chips sat at "…" on every such site).
+ {
+ const cookieSite = load({}, { wallet: { status: 200, body: data } });
+ cookieSite.navbar.init({ ...base, token: null });
+ await settle();
+ const i = cookieSite.calls.findIndex((u) => u.endsWith('/api/v1/me/wallet'));
+ assert(i >= 0, 'asked without a token');
+ assert.strictEqual(cookieSite.inits[i].credentials, 'include', "the person's Network cookie goes along");
+ assert(!(cookieSite.inits[i].headers && cookieSite.inits[i].headers.Authorization), 'no Authorization header');
+ assert.match(cookieSite.navs[cookieSite.navs.length - 1].innerHTML, /data-chip-id="(coins|vibes|balance)"/, "the chips show");
+ const signedOut = load({}, { wallet: { status: 401, body: { error: 'sign in' } } });
+ signedOut.navbar.init({ ...base, token: null });
+ await settle();
+ const last = signedOut.navs[signedOut.navs.length - 1].innerHTML;
+ assert(!/data-chip-id="(coins|vibes|balance)"/.test(last), 'no chips when the wallet cannot be read');
+ assert(!last.includes('…'), 'and no "…" left behind');
+ }
+ console.log('wallet chips: one Billing read, all zeros supported, Balance promoted for AI users, signout clears chips, cookie sites read by cookie');
})().catch((e) => { console.error(e); process.exit(1); });
})().catch((e) => { console.error(e); process.exit(1); });