openvibers/openvibe.shared Commits
Profile panel: data-ov-profile="me" shows whoever is signed in on the site (its /auth/me), so cached pages can show your stuff (#115)
Changes
@@@ -10,7 +10,8 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).draws someone's worn items, their counts of items, badges, achievements and games, their latest unlock per game andlinks to their profile and inventory, from OpenVibe.Network's public profile (which gathers Inventory, Quest andPlay since Network#333 and Play#38). Steam-style, so Games, Quest and any site can show it (the owner, 2026-10-25).-- `test/profile-panel.test.js`.++ `data-ov-profile="me"` is whoever is signed in on that site (its own `/auth/me`), so a page cached for everyone can++ show "your stuff" (a `hidden` element appears once drawn). `test/profile-panel.test.js`.- **One lit row in the menus on phones** (`navbar.js`). A tap left `:hover` on the row it touched, so the "all sites"launcher showed two highlighted rows: the site you are on and the last one your finger crossed (the owner, 2026-10-25,on openre.stream). The launcher's and the phone drawer's hover backgrounds now apply only under `@media (hover:@@@ -710,7 +710,8 @@ able to see your inventory anywhere"). Put a fallback link in an element and loa```Every `[data-ov-profile]` on the page is drawn once loaded; `OpenVibeProfilePanel.render(el, username)` draws one added--later. It reads OpenVibe.Network's public profile (`GET https://openvibe.network/api/v1/profiles/:username`, CORS++later. `data-ov-profile="me"` is whoever is signed in on that site (its own `/auth/me`, the navbar's session lookup), so a++page cached for everyone can still show "your stuff": give the element `hidden` and it appears only once drawn. It reads OpenVibe.Network's public profile (`GET https://openvibe.network/api/v1/profiles/:username`, CORSopen, no credentials), which gathers OpenVibe.Inventory, OpenVibe.Quest and OpenVibe.Play. Values are text, links onlyto OpenVibe's own https hosts, pictures only from openvibe.media and openvibe.network; a private profile shows its nameand picture; no answer leaves the fallback link. `window.OpenVibeProfilePanelConfig = { api }` points it at another@@@ -6,6 +6,9 @@* <div data-ov-profile="ana"><a href="https://openvibe.network/@ana">Ana's profile</a></div>* <script src="/shared/profile-panel.js" defer></script>* OpenVibeProfilePanel.render(el, 'ana') or every [data-ov-profile] on the page, by itself, once loaded++ * <section data-ov-profile="me" hidden>…</section> whoever is signed in on this site (its own /auth/me, the navbar's++ * session lookup), so a cached page can show "your stuff"; shown++ * only once drawn, left as it was for someone signed out** It reads OpenVibe.Network's public profile (GET https://openvibe.network/api/v1/profiles/:username: profile@1, CORS* open, no credentials, cached 60 s by the Network), which gathers Inventory, Quest and Play. The panel replaces the@@@ -91,6 +94,7 @@if (p.private) {card.appendChild(el('p', 'ovpp-note', 'This profile is private.'));target.replaceChildren(card);++ target.hidden = false;return card;}const ach = p.achievements || { total: 0, games: [] };@@@ -143,13 +147,29 @@if (p.inventory_url) { foot.appendChild(root.document.createTextNode(' \u00b7 ')); foot.appendChild(link(p.inventory_url, null, 'Inventory')); }card.appendChild(foot);target.replaceChildren(card);++ target.hidden = false;return card;}/** Draw `username`'s panel into `target`. → the panel, or null (the fallback stays) when there is no answer. */++ /** The signed-in person's username from this site's own /auth/me (the navbar's session lookup), once a page. */++ let mePromise = null;++ function me() {++ if (!mePromise) {++ mePromise = fetch('/auth/me', { credentials: 'same-origin', headers: { Accept: 'application/json' } })++ .then((r) => (r.ok ? r.json() : null)).then((b) => {++ const u = b && b.user;++ const name = u && (u.username || u.name);++ return NAME.test(String(name || '')) ? String(name) : null;++ }).catch(() => null);++ }++ return mePromise;++ }++async function render(target, username) {-- const name = String(username || '');-- if (!target || !NAME.test(name)) return null;++ // "me": whoever is signed in on this site; nobody signed in leaves the element as it was (hidden stays hidden).++ const name = String(username || '') === 'me' ? await me() : String(username || '');++ if (!target || !NAME.test(String(name || ''))) return null;const p = await load(name);return p ? draw(target, p) : null;}@@@ -2,7 +2,8 @@// profile-panel.js in a real headless Chrome at phone width: [data-ov-profile] elements become a person's stuff from// the Network's public profile JSON (worn items, counts, achievements by game, badges, links); every value is text// (a name with markup stays text), only https links on OpenVibe's own hosts become hrefs, a private profile shows its--// name only, a missing one leaves the fallback link, and nothing on the panel widens the page past the phone.++// name only, a missing one leaves the fallback link, "me" is whoever the site's /auth/me names (a hidden element shows++// once drawn), and nothing on the panel widens the page past the phone.// Skipped (exit 0) without Chrome or with OV_SKIP_BROWSER=1.const assert = require('assert');const fs = require('fs');@@@ -32,6 +33,7 @@ const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta n<body style="margin:0;padding:16px"><div id="ana" data-ov-profile="ana"><a href="https://openvibe.network/@ana">Ana</a></div><div id="priv" data-ov-profile="hidden"><a href="https://openvibe.network/@hidden">Hidden</a></div><div id="none" data-ov-profile="nobody"><a href="https://openvibe.network/@nobody">Nobody</a></div>++<section id="me" data-ov-profile="me" hidden><a href="https://openvibe.network/my">Your stuff</a></section><script>window.OpenVibeProfilePanelConfig = { api: location.origin };</script><script src="/shared/profile-panel.js" defer></script></body></html>`;const server = http.createServer((req, res) => {@@@ -39,6 +41,9 @@ const server = http.createServer((req, res) => {const json = (s, o) => { res.writeHead(s, { 'content-type': 'application/json' }); res.end(JSON.stringify(o)); };if (u.pathname === '/') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(PAGE); }if (u.pathname === '/api/v1/profiles/ana') return json(200, { profile: ANA });++ // Signed in on the main page; signed out on /out (told apart by the page that asks).++ if (u.pathname === '/auth/me') return json(200, { user: /\/out$/.test(req.headers.referer || '') ? null : { id: 7, username: 'ana' } });++ if (u.pathname === '/out') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(PAGE.replace(/<div id="ana"[\s\S]*?<\/section>/, '<section id="me" data-ov-profile="me" hidden><a href="https://openvibe.network/my">Your stuff</a></section>')); }if (u.pathname === '/api/v1/profiles/hidden') return json(200, { profile: { username: 'hidden', display_name: 'Hidden', profile_url: 'https://openvibe.network/@hidden', private: true } });if (u.pathname.startsWith('/api/v1/profiles/')) return json(404, { error: 'not_found' });if (u.pathname.startsWith('/avatar/')) { res.writeHead(204); return res.end(); }@@@ -56,7 +61,7 @@ const server = http.createServer((req, res) => {const context = await browser.newContext();const p = await h.openPage(browser, context, { width: 360, height: 900 });await p.goto(`${base}/`, { maxMs: 5000 });-- const got = JSON.parse(await p.evaluate(`new Promise((r) => { const t = setInterval(() => { if (document.querySelector('#ana .ovpp') && document.querySelector('#priv .ovpp')) { clearInterval(t); setTimeout(() => {++ const got = JSON.parse(await p.evaluate(`new Promise((r) => { const t = setInterval(() => { if (document.querySelector('#ana .ovpp') && document.querySelector('#priv .ovpp') && document.querySelector('#me .ovpp')) { clearInterval(t); setTimeout(() => {const ana = document.querySelector('#ana .ovpp');r(JSON.stringify({name: ana.querySelector('.ovpp-who b').textContent,@@@ -70,6 +75,7 @@ const server = http.createServer((req, res) => {priv: document.querySelector('#priv .ovpp').textContent,none: document.getElementById('none').innerHTML,wide: document.documentElement.scrollWidth,++ me: [document.getElementById('me').hidden, document.querySelector('#me .ovpp-who small').textContent],}));}, 100); } }, 30); })`));assert.strictEqual(got.name, 'Ana <img src=x onerror=alert(1)>', 'a name with markup stays text');@@@ -83,6 +89,10 @@ const server = http.createServer((req, res) => {assert.match(got.priv, /Hidden[\s\S]*This profile is private\./);assert.match(got.none, /href="https:\/\/openvibe\.network\/@nobody"/, 'no profile: the fallback link stays');assert.ok(got.wide <= 360, `fits a 360 px phone (${got.wide})`);++ assert.deepStrictEqual(got.me, [false, '@ana'], '"me" is whoever /auth/me names, shown once drawn');++ await p.goto(`${base}/out`, { maxMs: 5000 });++ const out = await p.evaluate(`new Promise((r) => setTimeout(() => r(JSON.stringify([document.getElementById('me').hidden, !!document.querySelector('#me .ovpp')])), 700))`);++ assert.deepStrictEqual(JSON.parse(out), [true, false], 'signed out: "me" stays hidden and undrawn');console.log('profile-panel: ok');} finally {await browser.close();
@@ -10,7 +10,8 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
@@ -10,7 +10,8 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
draws someone's worn items, their counts of items, badges, achievements and games, their latest unlock per game and
draws someone's worn items, their counts of items, badges, achievements and games, their latest unlock per game and
links to their profile and inventory, from OpenVibe.Network's public profile (which gathers Inventory, Quest and
links to their profile and inventory, from OpenVibe.Network's public profile (which gathers Inventory, Quest and
Play since Network#333 and Play#38). Steam-style, so Games, Quest and any site can show it (the owner, 2026-10-25).
Play since Network#333 and Play#38). Steam-style, so Games, Quest and any site can show it (the owner, 2026-10-25).
- `test/profile-panel.test.js`.
+ `data-ov-profile="me"` is whoever is signed in on that site (its own `/auth/me`), so a page cached for everyone can
+ show "your stuff" (a `hidden` element appears once drawn). `test/profile-panel.test.js`.
- **One lit row in the menus on phones** (`navbar.js`). A tap left `:hover` on the row it touched, so the "all sites"
- **One lit row in the menus on phones** (`navbar.js`). A tap left `:hover` on the row it touched, so the "all sites"
launcher showed two highlighted rows: the site you are on and the last one your finger crossed (the owner, 2026-10-25,
launcher showed two highlighted rows: the site you are on and the last one your finger crossed (the owner, 2026-10-25,
on openre.stream). The launcher's and the phone drawer's hover backgrounds now apply only under `@media (hover:
on openre.stream). The launcher's and the phone drawer's hover backgrounds now apply only under `@media (hover:
@@ -710,7 +710,8 @@ able to see your inventory anywhere"). Put a fallback link in an element and loa
@@ -710,7 +710,8 @@ able to see your inventory anywhere"). Put a fallback link in an element and loa
```
```
Every `[data-ov-profile]` on the page is drawn once loaded; `OpenVibeProfilePanel.render(el, username)` draws one added
Every `[data-ov-profile]` on the page is drawn once loaded; `OpenVibeProfilePanel.render(el, username)` draws one added
-later. It reads OpenVibe.Network's public profile (`GET https://openvibe.network/api/v1/profiles/:username`, CORS
+later. `data-ov-profile="me"` is whoever is signed in on that site (its own `/auth/me`, the navbar's session lookup), so a
+page cached for everyone can still show "your stuff": give the element `hidden` and it appears only once drawn. It reads OpenVibe.Network's public profile (`GET https://openvibe.network/api/v1/profiles/:username`, CORS
open, no credentials), which gathers OpenVibe.Inventory, OpenVibe.Quest and OpenVibe.Play. Values are text, links only
open, no credentials), which gathers OpenVibe.Inventory, OpenVibe.Quest and OpenVibe.Play. Values are text, links only
to OpenVibe's own https hosts, pictures only from openvibe.media and openvibe.network; a private profile shows its name
to OpenVibe's own https hosts, pictures only from openvibe.media and openvibe.network; a private profile shows its name
and picture; no answer leaves the fallback link. `window.OpenVibeProfilePanelConfig = { api }` points it at another
and picture; no answer leaves the fallback link. `window.OpenVibeProfilePanelConfig = { api }` points it at another
@@ -6,6 +6,9 @@
@@ -6,6 +6,9 @@
* <div data-ov-profile="ana"><a href="https://openvibe.network/@ana">Ana's profile</a></div>
* <div data-ov-profile="ana"><a href="https://openvibe.network/@ana">Ana's profile</a></div>
* <script src="/shared/profile-panel.js" defer></script>
* <script src="/shared/profile-panel.js" defer></script>
* OpenVibeProfilePanel.render(el, 'ana') or every [data-ov-profile] on the page, by itself, once loaded
* OpenVibeProfilePanel.render(el, 'ana') or every [data-ov-profile] on the page, by itself, once loaded
+ * <section data-ov-profile="me" hidden>…</section> whoever is signed in on this site (its own /auth/me, the navbar's
+ * session lookup), so a cached page can show "your stuff"; shown
+ * only once drawn, left as it was for someone signed out
*
*
* It reads OpenVibe.Network's public profile (GET https://openvibe.network/api/v1/profiles/:username: profile@1, CORS
* It reads OpenVibe.Network's public profile (GET https://openvibe.network/api/v1/profiles/:username: profile@1, CORS
* open, no credentials, cached 60 s by the Network), which gathers Inventory, Quest and Play. The panel replaces the
* open, no credentials, cached 60 s by the Network), which gathers Inventory, Quest and Play. The panel replaces the
@@ -91,6 +94,7 @@
@@ -91,6 +94,7 @@
if (p.private) {
if (p.private) {
card.appendChild(el('p', 'ovpp-note', 'This profile is private.'));
card.appendChild(el('p', 'ovpp-note', 'This profile is private.'));
target.replaceChildren(card);
target.replaceChildren(card);
+ target.hidden = false;
return card;
return card;
}
}
const ach = p.achievements || { total: 0, games: [] };
const ach = p.achievements || { total: 0, games: [] };
@@ -143,13 +147,29 @@
@@ -143,13 +147,29 @@
if (p.inventory_url) { foot.appendChild(root.document.createTextNode(' \u00b7 ')); foot.appendChild(link(p.inventory_url, null, 'Inventory')); }
if (p.inventory_url) { foot.appendChild(root.document.createTextNode(' \u00b7 ')); foot.appendChild(link(p.inventory_url, null, 'Inventory')); }
card.appendChild(foot);
card.appendChild(foot);
target.replaceChildren(card);
target.replaceChildren(card);
+ target.hidden = false;
return card;
return card;
}
}
/** Draw `username`'s panel into `target`. → the panel, or null (the fallback stays) when there is no answer. */
/** Draw `username`'s panel into `target`. → the panel, or null (the fallback stays) when there is no answer. */
+ /** The signed-in person's username from this site's own /auth/me (the navbar's session lookup), once a page. */
+ let mePromise = null;
+ function me() {
+ if (!mePromise) {
+ mePromise = fetch('/auth/me', { credentials: 'same-origin', headers: { Accept: 'application/json' } })
+ .then((r) => (r.ok ? r.json() : null)).then((b) => {
+ const u = b && b.user;
+ const name = u && (u.username || u.name);
+ return NAME.test(String(name || '')) ? String(name) : null;
+ }).catch(() => null);
+ }
+ return mePromise;
+ }
+
async function render(target, username) {
async function render(target, username) {
- const name = String(username || '');
- if (!target || !NAME.test(name)) return null;
+ // "me": whoever is signed in on this site; nobody signed in leaves the element as it was (hidden stays hidden).
+ const name = String(username || '') === 'me' ? await me() : String(username || '');
+ if (!target || !NAME.test(String(name || ''))) return null;
const p = await load(name);
const p = await load(name);
return p ? draw(target, p) : null;
return p ? draw(target, p) : null;
}
}
@@ -2,7 +2,8 @@
@@ -2,7 +2,8 @@
// profile-panel.js in a real headless Chrome at phone width: [data-ov-profile] elements become a person's stuff from
// profile-panel.js in a real headless Chrome at phone width: [data-ov-profile] elements become a person's stuff from
// the Network's public profile JSON (worn items, counts, achievements by game, badges, links); every value is text
// the Network's public profile JSON (worn items, counts, achievements by game, badges, links); every value is text
// (a name with markup stays text), only https links on OpenVibe's own hosts become hrefs, a private profile shows its
// (a name with markup stays text), only https links on OpenVibe's own hosts become hrefs, a private profile shows its
-// name only, a missing one leaves the fallback link, and nothing on the panel widens the page past the phone.
+// name only, a missing one leaves the fallback link, "me" is whoever the site's /auth/me names (a hidden element shows
+// once drawn), and nothing on the panel widens the page past the phone.
// Skipped (exit 0) without Chrome or with OV_SKIP_BROWSER=1.
// Skipped (exit 0) without Chrome or with OV_SKIP_BROWSER=1.
const assert = require('assert');
const assert = require('assert');
const fs = require('fs');
const fs = require('fs');
@@ -32,6 +33,7 @@ const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta n
@@ -32,6 +33,7 @@ const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta n
<body style="margin:0;padding:16px"><div id="ana" data-ov-profile="ana"><a href="https://openvibe.network/@ana">Ana</a></div>
<body style="margin:0;padding:16px"><div id="ana" data-ov-profile="ana"><a href="https://openvibe.network/@ana">Ana</a></div>
<div id="priv" data-ov-profile="hidden"><a href="https://openvibe.network/@hidden">Hidden</a></div>
<div id="priv" data-ov-profile="hidden"><a href="https://openvibe.network/@hidden">Hidden</a></div>
<div id="none" data-ov-profile="nobody"><a href="https://openvibe.network/@nobody">Nobody</a></div>
<div id="none" data-ov-profile="nobody"><a href="https://openvibe.network/@nobody">Nobody</a></div>
+<section id="me" data-ov-profile="me" hidden><a href="https://openvibe.network/my">Your stuff</a></section>
<script>window.OpenVibeProfilePanelConfig = { api: location.origin };</script>
<script>window.OpenVibeProfilePanelConfig = { api: location.origin };</script>
<script src="/shared/profile-panel.js" defer></script></body></html>`;
<script src="/shared/profile-panel.js" defer></script></body></html>`;
const server = http.createServer((req, res) => {
const server = http.createServer((req, res) => {
@@ -39,6 +41,9 @@ const server = http.createServer((req, res) => {
@@ -39,6 +41,9 @@ const server = http.createServer((req, res) => {
const json = (s, o) => { res.writeHead(s, { 'content-type': 'application/json' }); res.end(JSON.stringify(o)); };
const json = (s, o) => { res.writeHead(s, { 'content-type': 'application/json' }); res.end(JSON.stringify(o)); };
if (u.pathname === '/') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(PAGE); }
if (u.pathname === '/') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(PAGE); }
if (u.pathname === '/api/v1/profiles/ana') return json(200, { profile: ANA });
if (u.pathname === '/api/v1/profiles/ana') return json(200, { profile: ANA });
+ // Signed in on the main page; signed out on /out (told apart by the page that asks).
+ if (u.pathname === '/auth/me') return json(200, { user: /\/out$/.test(req.headers.referer || '') ? null : { id: 7, username: 'ana' } });
+ if (u.pathname === '/out') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(PAGE.replace(/<div id="ana"[\s\S]*?<\/section>/, '<section id="me" data-ov-profile="me" hidden><a href="https://openvibe.network/my">Your stuff</a></section>')); }
if (u.pathname === '/api/v1/profiles/hidden') return json(200, { profile: { username: 'hidden', display_name: 'Hidden', profile_url: 'https://openvibe.network/@hidden', private: true } });
if (u.pathname === '/api/v1/profiles/hidden') return json(200, { profile: { username: 'hidden', display_name: 'Hidden', profile_url: 'https://openvibe.network/@hidden', private: true } });
if (u.pathname.startsWith('/api/v1/profiles/')) return json(404, { error: 'not_found' });
if (u.pathname.startsWith('/api/v1/profiles/')) return json(404, { error: 'not_found' });
if (u.pathname.startsWith('/avatar/')) { res.writeHead(204); return res.end(); }
if (u.pathname.startsWith('/avatar/')) { res.writeHead(204); return res.end(); }
@@ -56,7 +61,7 @@ const server = http.createServer((req, res) => {
@@ -56,7 +61,7 @@ const server = http.createServer((req, res) => {
const context = await browser.newContext();
const context = await browser.newContext();
const p = await h.openPage(browser, context, { width: 360, height: 900 });
const p = await h.openPage(browser, context, { width: 360, height: 900 });
await p.goto(`${base}/`, { maxMs: 5000 });
await p.goto(`${base}/`, { maxMs: 5000 });
- const got = JSON.parse(await p.evaluate(`new Promise((r) => { const t = setInterval(() => { if (document.querySelector('#ana .ovpp') && document.querySelector('#priv .ovpp')) { clearInterval(t); setTimeout(() => {
+ const got = JSON.parse(await p.evaluate(`new Promise((r) => { const t = setInterval(() => { if (document.querySelector('#ana .ovpp') && document.querySelector('#priv .ovpp') && document.querySelector('#me .ovpp')) { clearInterval(t); setTimeout(() => {
const ana = document.querySelector('#ana .ovpp');
const ana = document.querySelector('#ana .ovpp');
r(JSON.stringify({
r(JSON.stringify({
name: ana.querySelector('.ovpp-who b').textContent,
name: ana.querySelector('.ovpp-who b').textContent,
@@ -70,6 +75,7 @@ const server = http.createServer((req, res) => {
@@ -70,6 +75,7 @@ const server = http.createServer((req, res) => {
priv: document.querySelector('#priv .ovpp').textContent,
priv: document.querySelector('#priv .ovpp').textContent,
none: document.getElementById('none').innerHTML,
none: document.getElementById('none').innerHTML,
wide: document.documentElement.scrollWidth,
wide: document.documentElement.scrollWidth,
+ me: [document.getElementById('me').hidden, document.querySelector('#me .ovpp-who small').textContent],
}));
}));
}, 100); } }, 30); })`));
}, 100); } }, 30); })`));
assert.strictEqual(got.name, 'Ana <img src=x onerror=alert(1)>', 'a name with markup stays text');
assert.strictEqual(got.name, 'Ana <img src=x onerror=alert(1)>', 'a name with markup stays text');
@@ -83,6 +89,10 @@ const server = http.createServer((req, res) => {
@@ -83,6 +89,10 @@ const server = http.createServer((req, res) => {
assert.match(got.priv, /Hidden[\s\S]*This profile is private\./);
assert.match(got.priv, /Hidden[\s\S]*This profile is private\./);
assert.match(got.none, /href="https:\/\/openvibe\.network\/@nobody"/, 'no profile: the fallback link stays');
assert.match(got.none, /href="https:\/\/openvibe\.network\/@nobody"/, 'no profile: the fallback link stays');
assert.ok(got.wide <= 360, `fits a 360 px phone (${got.wide})`);
assert.ok(got.wide <= 360, `fits a 360 px phone (${got.wide})`);
+ assert.deepStrictEqual(got.me, [false, '@ana'], '"me" is whoever /auth/me names, shown once drawn');
+ await p.goto(`${base}/out`, { maxMs: 5000 });
+ const out = await p.evaluate(`new Promise((r) => setTimeout(() => r(JSON.stringify([document.getElementById('me').hidden, !!document.querySelector('#me .ovpp')])), 700))`);
+ assert.deepStrictEqual(JSON.parse(out), [true, false], 'signed out: "me" stays hidden and undrawn');
console.log('profile-panel: ok');
console.log('profile-panel: ok');
} finally {
} finally {
await browser.close();
await browser.close();