openvibers/openvibe.shared Commits

4.173.0: the wallet works on sites with their own cookie session; the sheet goes by its hash (#391)

OP OpenVibers committed 4 days ago · 50a92d1575 · 11 files, +72 −26

* 4.173.0: the wallet works on sites with their own cookie session; the sheet goes by its hash

* CHANGELOG: name the sites that keep the access cookie from page scripts

* Cached balances stay when a read fails; the phone-fit page answers the wallet; navbar budget 48.75 KB

An unreadable wallet hides the chips only when there is nothing to show. The phone-fit test's page
now answers the wallet read (an unreadable one shows no chips since this release). navbar.js is
48.6 KB brotli with the cookie mode and the two new content hashes.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex ee2502c..522acc5 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -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
ddiff --git a/README.md b/README.md
iindex 2c79f3f..428d1bc 100644
---- a/README.md
++++ b/README.md
@@@ -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 deployed
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
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex ca18a5e..7bd77ff 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -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)",
ddiff --git a/navbar.js b/navbar.js
iindex 62bc3e8..177c519 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -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() {
ddiff --git a/ov-wallet.js b/ov-wallet.js
iindex 753cb1a..1c69dce 100644
---- a/ov-wallet.js
++++ b/ov-wallet.js
@@@ -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.');
ddiff --git a/package-lock.json b/package-lock.json
iindex 38007d4..16026c0 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -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",
ddiff --git a/package.json b/package.json
iindex 007b8a5..86fbb28 100644
---- a/package.json
++++ b/package.json
@@@ -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": {
ddiff --git a/scripts/sibling-revs.js b/scripts/sibling-revs.js
iindex 9a2b6e1..60f17ea 100644
---- a/scripts/sibling-revs.js
++++ b/scripts/sibling-revs.js
@@@ -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'] },
ddiff --git a/test/nav-icons.test.js b/test/nav-icons.test.js
iindex 3b3e900..755e354 100644
---- a/test/nav-icons.test.js
++++ b/test/nav-icons.test.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 ──────────────────────────
{
ddiff --git a/test/navbar-phone-fit-chrome.test.js b/test/navbar-phone-fit-chrome.test.js
iindex a19ffa9..ab9a530 100644
---- a/test/navbar-phone-fit-chrome.test.js
++++ b/test/navbar-phone-fit-chrome.test.js
@@@ -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>
ddiff --git a/test/navbar-wallet-chips.test.js b/test/navbar-wallet-chips.test.js
iindex bf94df1..6f391f2 100644
---- a/test/navbar-wallet-chips.test.js
++++ b/test/navbar-wallet-chips.test.js
@@@ -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.ne
assert.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); });