openvibers/openvibe.network Commits
Network: a profile wears what its person wears — their name effect on their name (openvibe-shared items.css) and their hat on their picture (#100)
Changes
@@@ -28,6 +28,7 @@ const TIMEOUT_MS = 4000;const MAX_ITEMS = 400; // two pages of Inventory's 200; the rest is one link away on OpenVibe.Inventoryconst SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;const COLOR_RE = /^#[0-9a-fA-F]{6}$/;++const NAME_FX_RE = /^name-fx-[a-z]{2,24}$/; // live.name_effect.css@1 tokens (openvibe-shared items.css)const RARITY = ['common', 'uncommon', 'rare', 'epic', 'legendary'];const RARITY_LABEL = { common: 'Common', uncommon: 'Uncommon', rare: 'Rare', epic: 'Epic', legendary: 'Legendary' };const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];@@@ -217,6 +218,12 @@ ${sorted.map((g) => {function renderPage(u, inv) {const name = u.display_name || u.username;const isPublic = !!inv;++ // What they wear shows on the profile itself: the name effect on their name (items.css), the hat on the picture.++ const worn = (kind) => (isPublic ? inv.showcase.find((i) => i.kind === kind) : null);++ const fx = worn('live.name_effect');++ const fxClass = fx && NAME_FX_RE.test(String((fx.art || {}).token || '')) ? fx.art.token : null;++ const hat = worn('live.hat');++ const hatGlyph = hat && (hat.art || {}).emoji ? String(hat.art.emoji).slice(0, 8) : null;const color = COLOR_RE.test(String(u.profile_color || '')) ? u.profile_color : '#3b82f6';const since = monthOf(u.created_at);const bio = isPublic ? String(u.bio || '').trim().slice(0, 500) : '';@@@ -253,6 +260,7 @@ ${isPublic ? '' : '<meta name="robots" content="noindex">'}${require('openvibe-shared/app-icon').headTags({ site: 'network', iconBase: '/assets' })}<meta name="color-scheme" content="dark light"><script src="/shared/theme-loader.js" defer></script>++${fxClass ? '<link rel="stylesheet" href="/shared/items.css">' : ''}${ld}<style>*,*::before,*::after{box-sizing:border-box}@@@ -264,6 +272,7 @@ a{color:var(--accent)}.pf-hero{position:relative;border:1px solid var(--border);border-radius:18px;overflow:hidden;background:var(--bg-secondary)}.pf-banner{height:96px;background:linear-gradient(120deg,color-mix(in srgb,var(--pf-color) 70%,transparent),color-mix(in srgb,var(--pf-color) 18%,transparent) 60%,transparent),var(--bg-card)}.pf-head{display:flex;gap:20px;align-items:flex-end;padding:0 24px 20px;margin-top:-52px;flex-wrap:wrap}++.pf-pic{position:relative;flex:none}.pf-hat{position:absolute;top:-22px;left:50%;transform:translateX(-50%) rotate(-12deg);font-size:40px;line-height:1;filter:drop-shadow(0 3px 4px rgba(0,0,0,.45));pointer-events:none}.pf-avatar{width:112px;height:112px;border-radius:50%;object-fit:cover;border:4px solid var(--bg-secondary);box-shadow:0 0 0 2px var(--pf-color);background:var(--bg-card);flex:none}.pf-who{flex:1;min-width:200px;padding-top:56px}.pf-who h1{margin:0;font-size:clamp(1.5rem,4vw,2.1rem);line-height:1.15;overflow-wrap:anywhere}@@@ -296,7 +305,7 @@ a{color:var(--accent)}.r-common{--rc:var(--r-common)}.r-uncommon{--rc:var(--r-uncommon)}.r-rare{--rc:var(--r-rare)}.r-epic{--rc:var(--r-epic)}.r-legendary{--rc:var(--r-legendary)}.pf-empty{color:var(--text-secondary);padding:18px;border:1px dashed var(--border);border-radius:12px;margin:0}.pf-note{color:var(--text-secondary);font-size:.85rem;margin:12px 0 0}--@media (max-width:560px){.pf-head{padding:0 16px 16px;gap:14px}.pf-avatar{width:92px;height:92px}.pf-who{padding-top:0;min-width:100%}.pf-bio,.pf-stats,.pf-private{padding-left:16px;padding-right:16px}.pf-grid{grid-template-columns:repeat(auto-fill,minmax(112px,1fr))}}++@media (max-width:560px){.pf-head{padding:0 16px 16px;gap:14px}.pf-avatar{width:92px;height:92px}.pf-hat{font-size:32px;top:-18px}.pf-who{padding-top:0;min-width:100%}.pf-bio,.pf-stats,.pf-private{padding-left:16px;padding-right:16px}.pf-grid{grid-template-columns:repeat(auto-fill,minmax(112px,1fr))}}</style></head><body>@@@ -306,8 +315,8 @@ ${require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links<section class="pf-hero" aria-label="Profile"><div class="pf-banner" aria-hidden="true"></div><div class="pf-head">--<img class="pf-avatar" src="${esc(avatarUrl(u.username))}" alt="" width="112" height="112">--<div class="pf-who"><h1>${esc(name)}</h1><p class="pf-handle">@${esc(u.username)}</p></div>++<div class="pf-pic"><img class="pf-avatar" src="${esc(avatarUrl(u.username))}" alt="" width="112" height="112">${hatGlyph ? `<span class="pf-hat" title="${esc(hat.name)}" aria-hidden="true">${esc(hatGlyph)}</span>` : ''}</div>++<div class="pf-who"><h1>${fxClass ? `<span class="ov-fx ${fxClass}" title="${esc(fx.name)}">${esc(name)}</span>` : esc(name)}</h1><p class="pf-handle">@${esc(u.username)}</p></div></div>${bio ? `<p class="pf-bio">${esc(bio)}</p>` : ''}${isPublic ? `<ul class="pf-stats">${stats}</ul>` : '<p class="pf-private">This profile is private.</p>'}@@@ -20,7 +20,7 @@ const KINDS = [];const DEFS = {itd_01JZ00000000000000000001R1: { id: 'itd_01JZ00000000000000000001R1', kind: 'live.hat', name: 'Royal Crown', rarity: 'legendary', art: { emoji: '👑' } },-- itd_01JZ00000000000000000001R2: { id: 'itd_01JZ00000000000000000001R2', kind: 'live.name_effect', name: 'Rainbow', rarity: 'epic', art: { emoji: '🌈' } },++ itd_01JZ00000000000000000001R2: { id: 'itd_01JZ00000000000000000001R2', kind: 'live.name_effect', name: 'Rainbow', rarity: 'epic', art: { emoji: '🌈', token: 'name-fx-rainbow' } },itd_01JZ00000000000000000001R3: { id: 'itd_01JZ00000000000000000001R3', kind: 'live.particle', name: 'Sparkles <b>', rarity: 'common', art: { emoji: '✨' } },};const INSTANCES = [@@@ -59,7 +59,7 @@ const INSTANCES = [const json = (s, o) => { res.writeHead(s, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(o)); };if (down) return json(503, { code: 'down' });if (u.pathname === '/api/v1/kinds') return json(200, { kinds: KINDS });-- if (u.pathname === `/api/v1/people/${A}/equipped`) return json(200, { subject: A, slots: { 'live.hat:hat': { instance_id: INSTANCES[0].id, definition_id: INSTANCES[0].definition_id } } });++ if (u.pathname === `/api/v1/people/${A}/equipped`) return json(200, { subject: A, slots: { 'live.hat:hat': { instance_id: INSTANCES[0].id, definition_id: INSTANCES[0].definition_id }, 'live.name_effect:name_effect': { instance_id: INSTANCES[1].id, definition_id: INSTANCES[1].definition_id, token: 'name-fx-rainbow' } } });if (u.pathname === `/api/v1/people/${A}/items`) return json(200, { subject: A, instances: INSTANCES, definitions: DEFS, next_cursor: null });return json(404, { code: 'inventory.unknown_subject' });});@@@ -80,7 +80,9 @@ const INSTANCES = [// A public profile.let r = await get('/@ana');assert.strictEqual(r.status, 200);-- assert.ok(r.text.includes('<h1>Ana</h1>') && r.text.includes('@Ana'));++ assert.ok(r.text.includes('<h1><span class="ov-fx name-fx-rainbow" title="Rainbow">Ana</span></h1>') && r.text.includes('@Ana'), 'her name wears its effect');++ assert.ok(r.text.includes('<link rel="stylesheet" href="/shared/items.css">'), 'with the shared effects stylesheet');++ assert.ok(/<span class="pf-hat" title="Royal Crown" aria-hidden="true">👑<\/span>/.test(r.text), 'her hat sits on her picture');assert.ok(!r.text.includes('<script>alert(1)</script>') && r.text.includes('I stream <script>alert(1)</script> & paint'), 'the bio is escaped');assert.ok(r.text.includes('Sparkles <b>') && !r.text.includes('Sparkles <b>'), 'item names are escaped');assert.ok(r.text.includes('id="pf-wearing"') && /pf-worn r-legendary[\s\S]*Royal Crown/.test(r.text), 'the crown is in the showcase');@@@ -102,7 +104,7 @@ const INSTANCES = [r = await get('/api/v1/profiles/Ana');assert.strictEqual(r.status, 200);const p = JSON.parse(r.text).profile;-- assert.deepStrictEqual([p.username, p.private, p.color, p.member_since, p.items.count, p.showcase.map((s) => s.name)], ['Ana', false, '#ff0066', '2026-10-01', 3, ['Royal Crown']]);++ assert.deepStrictEqual([p.username, p.private, p.color, p.member_since, p.items.count, p.showcase.map((s) => s.name)], ['Ana', false, '#ff0066', '2026-10-01', 3, ['Rainbow', 'Royal Crown']]);assert.strictEqual(p.avatar_url, 'https://openvibe.network/avatar/Ana?s=160');assert.strictEqual(p.inventory_url, `https://inventory.openvibe.network/u/${A}`);assert.deepStrictEqual(p.items.by_kind.map((k) => k.kind).sort(), ['live.hat', 'live.name_effect', 'live.particle']);@@@ -112,6 +114,7 @@ const INSTANCES = [assert.strictEqual(r.status, 200);assert.ok(r.text.includes('This profile is private.') && r.text.includes('name="robots" content="noindex"'));assert.ok(!r.text.includes('secret bio') && !r.text.includes('Member since') && !r.text.includes('class="pf-grid"'));++ assert.ok(r.text.includes('<h1>Hidden</h1>') && !r.text.includes('class="pf-hat"') && !r.text.includes('/shared/items.css'), 'a hidden profile wears nothing');r = await get('/api/v1/profiles/hidden');assert.deepStrictEqual(JSON.parse(r.text).profile, { username: 'hidden', display_name: 'Hidden', avatar_url: 'https://openvibe.network/avatar/hidden?s=160', profile_url: 'https://openvibe.network/@hidden', private: true });
@@ -28,6 +28,7 @@ const TIMEOUT_MS = 4000;
@@ -28,6 +28,7 @@ const TIMEOUT_MS = 4000;
const MAX_ITEMS = 400; // two pages of Inventory's 200; the rest is one link away on OpenVibe.Inventory
const MAX_ITEMS = 400; // two pages of Inventory's 200; the rest is one link away on OpenVibe.Inventory
const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
const COLOR_RE = /^#[0-9a-fA-F]{6}$/;
const COLOR_RE = /^#[0-9a-fA-F]{6}$/;
+const NAME_FX_RE = /^name-fx-[a-z]{2,24}$/; // live.name_effect.css@1 tokens (openvibe-shared items.css)
const RARITY = ['common', 'uncommon', 'rare', 'epic', 'legendary'];
const RARITY = ['common', 'uncommon', 'rare', 'epic', 'legendary'];
const RARITY_LABEL = { common: 'Common', uncommon: 'Uncommon', rare: 'Rare', epic: 'Epic', legendary: 'Legendary' };
const RARITY_LABEL = { common: 'Common', uncommon: 'Uncommon', rare: 'Rare', epic: 'Epic', legendary: 'Legendary' };
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
@@ -217,6 +218,12 @@ ${sorted.map((g) => {
@@ -217,6 +218,12 @@ ${sorted.map((g) => {
function renderPage(u, inv) {
function renderPage(u, inv) {
const name = u.display_name || u.username;
const name = u.display_name || u.username;
const isPublic = !!inv;
const isPublic = !!inv;
+ // What they wear shows on the profile itself: the name effect on their name (items.css), the hat on the picture.
+ const worn = (kind) => (isPublic ? inv.showcase.find((i) => i.kind === kind) : null);
+ const fx = worn('live.name_effect');
+ const fxClass = fx && NAME_FX_RE.test(String((fx.art || {}).token || '')) ? fx.art.token : null;
+ const hat = worn('live.hat');
+ const hatGlyph = hat && (hat.art || {}).emoji ? String(hat.art.emoji).slice(0, 8) : null;
const color = COLOR_RE.test(String(u.profile_color || '')) ? u.profile_color : '#3b82f6';
const color = COLOR_RE.test(String(u.profile_color || '')) ? u.profile_color : '#3b82f6';
const since = monthOf(u.created_at);
const since = monthOf(u.created_at);
const bio = isPublic ? String(u.bio || '').trim().slice(0, 500) : '';
const bio = isPublic ? String(u.bio || '').trim().slice(0, 500) : '';
@@ -253,6 +260,7 @@ ${isPublic ? '' : '<meta name="robots" content="noindex">'}
@@ -253,6 +260,7 @@ ${isPublic ? '' : '<meta name="robots" content="noindex">'}
${require('openvibe-shared/app-icon').headTags({ site: 'network', iconBase: '/assets' })}
${require('openvibe-shared/app-icon').headTags({ site: 'network', iconBase: '/assets' })}
<meta name="color-scheme" content="dark light">
<meta name="color-scheme" content="dark light">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
+${fxClass ? '<link rel="stylesheet" href="/shared/items.css">' : ''}
${ld}
${ld}
<style>
<style>
*,*::before,*::after{box-sizing:border-box}
*,*::before,*::after{box-sizing:border-box}
@@ -264,6 +272,7 @@ a{color:var(--accent)}
@@ -264,6 +272,7 @@ a{color:var(--accent)}
.pf-hero{position:relative;border:1px solid var(--border);border-radius:18px;overflow:hidden;background:var(--bg-secondary)}
.pf-hero{position:relative;border:1px solid var(--border);border-radius:18px;overflow:hidden;background:var(--bg-secondary)}
.pf-banner{height:96px;background:linear-gradient(120deg,color-mix(in srgb,var(--pf-color) 70%,transparent),color-mix(in srgb,var(--pf-color) 18%,transparent) 60%,transparent),var(--bg-card)}
.pf-banner{height:96px;background:linear-gradient(120deg,color-mix(in srgb,var(--pf-color) 70%,transparent),color-mix(in srgb,var(--pf-color) 18%,transparent) 60%,transparent),var(--bg-card)}
.pf-head{display:flex;gap:20px;align-items:flex-end;padding:0 24px 20px;margin-top:-52px;flex-wrap:wrap}
.pf-head{display:flex;gap:20px;align-items:flex-end;padding:0 24px 20px;margin-top:-52px;flex-wrap:wrap}
+.pf-pic{position:relative;flex:none}.pf-hat{position:absolute;top:-22px;left:50%;transform:translateX(-50%) rotate(-12deg);font-size:40px;line-height:1;filter:drop-shadow(0 3px 4px rgba(0,0,0,.45));pointer-events:none}
.pf-avatar{width:112px;height:112px;border-radius:50%;object-fit:cover;border:4px solid var(--bg-secondary);box-shadow:0 0 0 2px var(--pf-color);background:var(--bg-card);flex:none}
.pf-avatar{width:112px;height:112px;border-radius:50%;object-fit:cover;border:4px solid var(--bg-secondary);box-shadow:0 0 0 2px var(--pf-color);background:var(--bg-card);flex:none}
.pf-who{flex:1;min-width:200px;padding-top:56px}
.pf-who{flex:1;min-width:200px;padding-top:56px}
.pf-who h1{margin:0;font-size:clamp(1.5rem,4vw,2.1rem);line-height:1.15;overflow-wrap:anywhere}
.pf-who h1{margin:0;font-size:clamp(1.5rem,4vw,2.1rem);line-height:1.15;overflow-wrap:anywhere}
@@ -296,7 +305,7 @@ a{color:var(--accent)}
@@ -296,7 +305,7 @@ a{color:var(--accent)}
.r-common{--rc:var(--r-common)}.r-uncommon{--rc:var(--r-uncommon)}.r-rare{--rc:var(--r-rare)}.r-epic{--rc:var(--r-epic)}.r-legendary{--rc:var(--r-legendary)}
.r-common{--rc:var(--r-common)}.r-uncommon{--rc:var(--r-uncommon)}.r-rare{--rc:var(--r-rare)}.r-epic{--rc:var(--r-epic)}.r-legendary{--rc:var(--r-legendary)}
.pf-empty{color:var(--text-secondary);padding:18px;border:1px dashed var(--border);border-radius:12px;margin:0}
.pf-empty{color:var(--text-secondary);padding:18px;border:1px dashed var(--border);border-radius:12px;margin:0}
.pf-note{color:var(--text-secondary);font-size:.85rem;margin:12px 0 0}
.pf-note{color:var(--text-secondary);font-size:.85rem;margin:12px 0 0}
-@media (max-width:560px){.pf-head{padding:0 16px 16px;gap:14px}.pf-avatar{width:92px;height:92px}.pf-who{padding-top:0;min-width:100%}.pf-bio,.pf-stats,.pf-private{padding-left:16px;padding-right:16px}.pf-grid{grid-template-columns:repeat(auto-fill,minmax(112px,1fr))}}
+@media (max-width:560px){.pf-head{padding:0 16px 16px;gap:14px}.pf-avatar{width:92px;height:92px}.pf-hat{font-size:32px;top:-18px}.pf-who{padding-top:0;min-width:100%}.pf-bio,.pf-stats,.pf-private{padding-left:16px;padding-right:16px}.pf-grid{grid-template-columns:repeat(auto-fill,minmax(112px,1fr))}}
</style>
</style>
</head>
</head>
<body>
<body>
@@ -306,8 +315,8 @@ ${require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links
@@ -306,8 +315,8 @@ ${require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links
<section class="pf-hero" aria-label="Profile">
<section class="pf-hero" aria-label="Profile">
<div class="pf-banner" aria-hidden="true"></div>
<div class="pf-banner" aria-hidden="true"></div>
<div class="pf-head">
<div class="pf-head">
-<img class="pf-avatar" src="${esc(avatarUrl(u.username))}" alt="" width="112" height="112">
-<div class="pf-who"><h1>${esc(name)}</h1><p class="pf-handle">@${esc(u.username)}</p></div>
+<div class="pf-pic"><img class="pf-avatar" src="${esc(avatarUrl(u.username))}" alt="" width="112" height="112">${hatGlyph ? `<span class="pf-hat" title="${esc(hat.name)}" aria-hidden="true">${esc(hatGlyph)}</span>` : ''}</div>
+<div class="pf-who"><h1>${fxClass ? `<span class="ov-fx ${fxClass}" title="${esc(fx.name)}">${esc(name)}</span>` : esc(name)}</h1><p class="pf-handle">@${esc(u.username)}</p></div>
</div>
</div>
${bio ? `<p class="pf-bio">${esc(bio)}</p>` : ''}
${bio ? `<p class="pf-bio">${esc(bio)}</p>` : ''}
${isPublic ? `<ul class="pf-stats">${stats}</ul>` : '<p class="pf-private">This profile is private.</p>'}
${isPublic ? `<ul class="pf-stats">${stats}</ul>` : '<p class="pf-private">This profile is private.</p>'}
@@ -20,7 +20,7 @@ const KINDS = [
@@ -20,7 +20,7 @@ const KINDS = [
];
];
const DEFS = {
const DEFS = {
itd_01JZ00000000000000000001R1: { id: 'itd_01JZ00000000000000000001R1', kind: 'live.hat', name: 'Royal Crown', rarity: 'legendary', art: { emoji: '👑' } },
itd_01JZ00000000000000000001R1: { id: 'itd_01JZ00000000000000000001R1', kind: 'live.hat', name: 'Royal Crown', rarity: 'legendary', art: { emoji: '👑' } },
- itd_01JZ00000000000000000001R2: { id: 'itd_01JZ00000000000000000001R2', kind: 'live.name_effect', name: 'Rainbow', rarity: 'epic', art: { emoji: '🌈' } },
+ itd_01JZ00000000000000000001R2: { id: 'itd_01JZ00000000000000000001R2', kind: 'live.name_effect', name: 'Rainbow', rarity: 'epic', art: { emoji: '🌈', token: 'name-fx-rainbow' } },
itd_01JZ00000000000000000001R3: { id: 'itd_01JZ00000000000000000001R3', kind: 'live.particle', name: 'Sparkles <b>', rarity: 'common', art: { emoji: '✨' } },
itd_01JZ00000000000000000001R3: { id: 'itd_01JZ00000000000000000001R3', kind: 'live.particle', name: 'Sparkles <b>', rarity: 'common', art: { emoji: '✨' } },
};
};
const INSTANCES = [
const INSTANCES = [
@@ -59,7 +59,7 @@ const INSTANCES = [
@@ -59,7 +59,7 @@ const INSTANCES = [
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 (down) return json(503, { code: 'down' });
if (down) return json(503, { code: 'down' });
if (u.pathname === '/api/v1/kinds') return json(200, { kinds: KINDS });
if (u.pathname === '/api/v1/kinds') return json(200, { kinds: KINDS });
- if (u.pathname === `/api/v1/people/${A}/equipped`) return json(200, { subject: A, slots: { 'live.hat:hat': { instance_id: INSTANCES[0].id, definition_id: INSTANCES[0].definition_id } } });
+ if (u.pathname === `/api/v1/people/${A}/equipped`) return json(200, { subject: A, slots: { 'live.hat:hat': { instance_id: INSTANCES[0].id, definition_id: INSTANCES[0].definition_id }, 'live.name_effect:name_effect': { instance_id: INSTANCES[1].id, definition_id: INSTANCES[1].definition_id, token: 'name-fx-rainbow' } } });
if (u.pathname === `/api/v1/people/${A}/items`) return json(200, { subject: A, instances: INSTANCES, definitions: DEFS, next_cursor: null });
if (u.pathname === `/api/v1/people/${A}/items`) return json(200, { subject: A, instances: INSTANCES, definitions: DEFS, next_cursor: null });
return json(404, { code: 'inventory.unknown_subject' });
return json(404, { code: 'inventory.unknown_subject' });
});
});
@@ -80,7 +80,9 @@ const INSTANCES = [
@@ -80,7 +80,9 @@ const INSTANCES = [
// A public profile.
// A public profile.
let r = await get('/@ana');
let r = await get('/@ana');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
- assert.ok(r.text.includes('<h1>Ana</h1>') && r.text.includes('@Ana'));
+ assert.ok(r.text.includes('<h1><span class="ov-fx name-fx-rainbow" title="Rainbow">Ana</span></h1>') && r.text.includes('@Ana'), 'her name wears its effect');
+ assert.ok(r.text.includes('<link rel="stylesheet" href="/shared/items.css">'), 'with the shared effects stylesheet');
+ assert.ok(/<span class="pf-hat" title="Royal Crown" aria-hidden="true">👑<\/span>/.test(r.text), 'her hat sits on her picture');
assert.ok(!r.text.includes('<script>alert(1)</script>') && r.text.includes('I stream <script>alert(1)</script> & paint'), 'the bio is escaped');
assert.ok(!r.text.includes('<script>alert(1)</script>') && r.text.includes('I stream <script>alert(1)</script> & paint'), 'the bio is escaped');
assert.ok(r.text.includes('Sparkles <b>') && !r.text.includes('Sparkles <b>'), 'item names are escaped');
assert.ok(r.text.includes('Sparkles <b>') && !r.text.includes('Sparkles <b>'), 'item names are escaped');
assert.ok(r.text.includes('id="pf-wearing"') && /pf-worn r-legendary[\s\S]*Royal Crown/.test(r.text), 'the crown is in the showcase');
assert.ok(r.text.includes('id="pf-wearing"') && /pf-worn r-legendary[\s\S]*Royal Crown/.test(r.text), 'the crown is in the showcase');
@@ -102,7 +104,7 @@ const INSTANCES = [
@@ -102,7 +104,7 @@ const INSTANCES = [
r = await get('/api/v1/profiles/Ana');
r = await get('/api/v1/profiles/Ana');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
const p = JSON.parse(r.text).profile;
const p = JSON.parse(r.text).profile;
- assert.deepStrictEqual([p.username, p.private, p.color, p.member_since, p.items.count, p.showcase.map((s) => s.name)], ['Ana', false, '#ff0066', '2026-10-01', 3, ['Royal Crown']]);
+ assert.deepStrictEqual([p.username, p.private, p.color, p.member_since, p.items.count, p.showcase.map((s) => s.name)], ['Ana', false, '#ff0066', '2026-10-01', 3, ['Rainbow', 'Royal Crown']]);
assert.strictEqual(p.avatar_url, 'https://openvibe.network/avatar/Ana?s=160');
assert.strictEqual(p.avatar_url, 'https://openvibe.network/avatar/Ana?s=160');
assert.strictEqual(p.inventory_url, `https://inventory.openvibe.network/u/${A}`);
assert.strictEqual(p.inventory_url, `https://inventory.openvibe.network/u/${A}`);
assert.deepStrictEqual(p.items.by_kind.map((k) => k.kind).sort(), ['live.hat', 'live.name_effect', 'live.particle']);
assert.deepStrictEqual(p.items.by_kind.map((k) => k.kind).sort(), ['live.hat', 'live.name_effect', 'live.particle']);
@@ -112,6 +114,7 @@ const INSTANCES = [
@@ -112,6 +114,7 @@ const INSTANCES = [
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.ok(r.text.includes('This profile is private.') && r.text.includes('name="robots" content="noindex"'));
assert.ok(r.text.includes('This profile is private.') && r.text.includes('name="robots" content="noindex"'));
assert.ok(!r.text.includes('secret bio') && !r.text.includes('Member since') && !r.text.includes('class="pf-grid"'));
assert.ok(!r.text.includes('secret bio') && !r.text.includes('Member since') && !r.text.includes('class="pf-grid"'));
+ assert.ok(r.text.includes('<h1>Hidden</h1>') && !r.text.includes('class="pf-hat"') && !r.text.includes('/shared/items.css'), 'a hidden profile wears nothing');
r = await get('/api/v1/profiles/hidden');
r = await get('/api/v1/profiles/hidden');
assert.deepStrictEqual(JSON.parse(r.text).profile, { username: 'hidden', display_name: 'Hidden', avatar_url: 'https://openvibe.network/avatar/hidden?s=160', profile_url: 'https://openvibe.network/@hidden', private: true });
assert.deepStrictEqual(JSON.parse(r.text).profile, { username: 'hidden', display_name: 'Hidden', avatar_url: 'https://openvibe.network/avatar/hidden?s=160', profile_url: 'https://openvibe.network/@hidden', private: true });