openvibers/openvibe.network Commits

openvibe.my: the deletion banner asks Network directly, and avatars load (#648)

OP OpenVibers committed 6 days ago · 4282002d86 · 3 files, +32 −7

Signed in on openvibe.my (probe, production): GET /api/v1/account/deletion answered 401. account-safety.js used
location.origin, so on openvibe.my the call went to openvibe.my/api and was 308-redirected to openvibe.network,
and a browser drops the Authorization header on a cross-origin redirect. On openvibe.my it now calls
https://openvibe.network with the Bearer and credentials 'omit'. The page CSP's img-src gains openvibe.network
(the hub's avatars come from Network's /avatar; 'self' covered them only on openvibe.network).
Test: account-deletion-reauth (the openvibe.my call: URL, no cookie, the Bearer).

Changes

ddiff --git a/public/js/account-safety.js b/public/js/account-safety.js
iindex 149d612a..06983e56 100644
---- a/public/js/account-safety.js
++++ b/public/js/account-safety.js
@@@ -16,9 +16,18 @@
const REAUTH_WINDOW_S = 10 * 60;
const BANNER_ID = 'ov-deletion-banner';
++ // The account hub on its own origin (openvibe.my, 2026-11-07) asks Network's API directly: a relative call there is
++ // redirected to openvibe.network, and a browser drops the Authorization header on a cross-origin redirect (401).
++ function onMyHost() {
++ const h = root && root.location && root.location.hostname;
++ return h === 'openvibe.my' || h === 'www.openvibe.my';
++ }
function base() {
++ if (onMyHost()) return 'https://openvibe.network';
return (root && root.location && root.location.origin) || '';
}
++ // No cookie crosses to openvibe.network from openvibe.my: the Bearer token is the whole credential there.
++ function credentials() { return onMyHost() ? 'omit' : 'include'; }
function cookieToken() {
try {
@@@ -70,7 +79,7 @@
if (!t) return null;
const res = await fetch(base() + '/api/v1/account/deletion', {
headers: { Authorization: 'Bearer ' + t },
-- credentials: 'include',
++ credentials: credentials(),
});
if (!res.ok) return null;
const body = await res.json().catch(() => ({}));
@@@ -83,7 +92,7 @@
const res = await fetch(base() + '/api/v1/account/deletion', {
method: 'DELETE',
headers: { Authorization: 'Bearer ' + t },
-- credentials: 'include',
++ credentials: credentials(),
});
let body = {};
try { body = await res.json(); } catch { /* no body */ }
ddiff --git a/server/index.js b/server/index.js
iindex a66a667a..1bbfed12 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -125,7 +125,8 @@ app.use(helmet({
fontSrc: ["'self'", "fonts.gstatic.com"],
// Avatars and media live on openvibe.media; the home page's live-stream thumbnails come from openvibe.live and
// its "Right now" screenshot pastes from openvibe.community (blocked until 2026-11-06: the card showed nothing).
-- imgSrc: ["'self'", "data:", "blob:", "https://openvibe.media", "https://openvibe.live", "https://openvibe.community"],
++ // openvibe.network itself too: the account hub on openvibe.my draws avatars from Network's /avatar (2026-11-07).
++ imgSrc: ["'self'", "data:", "blob:", "https://openvibe.network", "https://openvibe.media", "https://openvibe.live", "https://openvibe.community"],
connectSrc: [
"'self'",
"https://openvibe.network",
ddiff --git a/test/account-deletion-reauth.test.js b/test/account-deletion-reauth.test.js
iindex cddd0710..8fc73d8b 100644
---- a/test/account-deletion-reauth.test.js
++++ b/test/account-deletion-reauth.test.js
@@@ -34,20 +34,20 @@ const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
// ── The browser helpers: one re-authentication URL, and the banner ───────────
const safety = read('public/js/account-safety.js');
--function safetyWindow() {
++function safetyWindow(location = { origin: 'https://openvibe.network', hostname: 'openvibe.network' }) {
const { window: win } = parseHTML('<!doctype html><html><body></body></html>');
const store = new Map();
const calls = [];
const root = {
document: win.document,
-- location: { origin: 'https://openvibe.network' },
++ location,
localStorage: {
getItem: (k) => (store.has(k) ? store.get(k) : null),
setItem: (k, v) => store.set(k, String(v)),
removeItem: (k) => store.delete(k),
},
fetch: async (url, opts = {}) => {
-- calls.push({ url: String(url), method: opts.method || 'GET', headers: opts.headers || {} });
++ calls.push({ url: String(url), method: opts.method || 'GET', headers: opts.headers || {}, credentials: opts.credentials });
if ((opts.method || 'GET') === 'DELETE') {
return { ok: true, status: 200, json: async () => ({ status: 'cancelled', deletion_id: 'del_x' }) };
}
@@@ -251,7 +251,22 @@ assert.strictEqual(reauthUrl('https://openvibe.network', '/my#delete'), 'https:/
assert.strictEqual(signInUrl({ loginUrl: 'https://openvibe.network', baseUrl: 'https://openvibe.network' }),
'https://openvibe.network/login?reauth=1&return=https%3A%2F%2Fopenvibe.network%2Fadmin%23ai');
--Promise.all([bannerTest, serverTest()]).then(() => console.log('account deletion reauth: all checks passed')).catch((err) => {
++// On openvibe.my (the account hub's own origin) the banner asks Network directly, with the Bearer and no cookie: a
++// relative call is redirected there and the browser drops the Authorization header on that redirect (a 401).
++const myHostTest = (async () => {
++ const { root, calls, api } = safetyWindow({ origin: 'https://openvibe.my', hostname: 'openvibe.my' });
++ root.localStorage.setItem('openvibe_accounts', JSON.stringify([{ id: 7, username: 'Me', token: 'tok-saved' }]));
++ root.localStorage.setItem('openvibe_active_account', '7');
++ calls.length = 0;
++ await api.deletionStatus();
++ const c = calls.find((x) => x.url.includes('/api/v1/account/deletion'));
++ assert.ok(c, 'the status was asked');
++ assert.strictEqual(c.url, 'https://openvibe.network/api/v1/account/deletion');
++ assert.strictEqual(c.credentials, 'omit', 'no cookie crosses from openvibe.my');
++ assert.strictEqual(c.headers.Authorization, 'Bearer tok-saved');
++})();
++
++Promise.all([bannerTest, myHostTest, serverTest()]).then(() => console.log('account deletion reauth: all checks passed')).catch((err) => {
console.error(err);
process.exitCode = 1;
});