openvibers/openvibe.network Commits
Move Inventory navigation and proxy routing into openvibe.my (#666)
Changes
@@@ -114,6 +114,21 @@ server {location ~* ^/(metrics|internal)(/|$) { return 404; }++ # Inventory keeps its prefix: the app serves assets, forms and SSO under it.++ # ^~ bypasses outer regex locations, so private endpoints are denied here too.++ location ^~ /inventory {++ if ($uri ~* "^/inventory/(internal|metrics)(/|$)") { return 404; }++ if ($uri ~ "^/inventory/\\.(?!well-known)") { return 404; }++ client_max_body_size 2m;++ proxy_pass http://127.0.0.1:5030;++ proxy_http_version 1.1;++ proxy_set_header Host $host;++ proxy_set_header X-Real-IP $remote_addr;++ proxy_set_header X-Forwarded-For $remote_addr;++ proxy_set_header CF-Connecting-IP $remote_addr;++ proxy_set_header X-Forwarded-Proto $scheme;++ }++# The hub's page, its sections, its /auth sign-in and every shared asset the page loads (css, js, shared,# img, fonts, icons, manifest, service worker) are Network's own responses for this Host.location / {@@@ -33,19 +33,19 @@ body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMa/* Section tabs */.section-tabs{display:flex;gap:4px;margin-bottom:24px;background:var(--bg-secondary);border-radius:10px;padding:4px;overflow-x:auto}--.section-tabs button{-- flex-shrink:0;padding:8px 16px;border:none;border-radius:8px;font-size:12px;font-weight:600;++.section-tabs :is(button,a){++ text-decoration:none;flex-shrink:0;padding:8px 16px;border:none;border-radius:8px;font-size:12px;font-weight:600;background:transparent;color:var(--text-muted);cursor:pointer;transition:all var(--ov-dur-2, .15s);white-space:nowrap;}.section-tabs button.active{background:var(--bg-card);color:var(--accent-light)}--.section-tabs button:hover:not(.active){color:var(--text)}++.section-tabs :is(button,a):hover:not(.active){color:var(--text)}/* On a computer the tabs stand as a sidebar beside the open section (L43): every setting in view, the page shorter. */@media (min-width:1000px){.page{max-width:1080px}.settings{display:grid;grid-template-columns:208px minmax(0,1fr);gap:28px;align-items:start}.settings .section-tabs{position:sticky;top:76px;flex-direction:column;gap:2px;margin:0;padding:6px;overflow:visible}-- .settings .section-tabs button{display:flex;align-items:center;gap:10px;width:100%;padding:10px 12px;font-size:13.5px;text-align:left}-- .settings .section-tabs button i{width:16px;text-align:center;opacity:.85}++ .settings .section-tabs :is(button,a){display:flex;align-items:center;gap:10px;width:100%;padding:10px 12px;font-size:13.5px;text-align:left}++ .settings .section-tabs :is(button,a) i{width:16px;text-align:center;opacity:.85}.settings .section-tabs button.active{box-shadow:inset 3px 0 0 var(--accent)}}@@@ -142,6 +142,7 @@<button onclick="showSection('apps',this)"><i class="fa-solid fa-plug"></i> Apps</button><button onclick="showSection('linked',this)"><i class="fa-solid fa-link"></i> Linked Services</button><button onclick="showSection('machines',this)"><i class="fa-solid fa-laptop"></i> Machines</button>++ <a href="/inventory"><i class="fa-solid fa-shirt"></i> Inventory</a><button onclick="showSection('security',this)"><i class="fa-solid fa-lock"></i> Security</button><button onclick="showSection('anon',this)"><i class="fa-solid fa-user-secret"></i> Anon Info</button></div>@@@ -73,7 +73,7 @@<p class="w-amount" id="w-coins-amount">—</p><p class="w-line">Earned by watching and chatting</p><div class="w-actions">-- <a class="ovk-btn ovk-btn--quiet" href="https://inventory.openvibe.network/" target="_blank" rel="noopener">Spend your OpenCoins</a>++ <a class="ovk-btn ovk-btn--quiet" href="https://openvibe.my/inventory" target="_blank" rel="noopener">Spend your OpenCoins</a></div></section></div>@@@ -173,7 +173,7 @@ async function seedDb(db, { log = console } = {}) {// for uploads and albums (OpenVibe.Pics, 2026-10-15).{ client_id: 'media-hub', name: 'OpenVibe.Download', redirect_uris: ['https://openvibe.download/auth/callback', 'https://openvibe.video/auth/callback', 'https://openvibe.pics/auth/callback'] },// The network-wide inventory (ADR-054, plan T21).-- { client_id: 'inventory', name: 'OpenVibe.Inventory', redirect_uris: ['https://inventory.openvibe.network/auth/callback'] },++ { client_id: 'inventory', name: 'OpenVibe.Inventory', redirect_uris: ['https://inventory.openvibe.network/auth/callback', 'https://openvibe.my/inventory/auth/callback'] },// Achievements and leaderboards for every game (Games ADR-0007 decision 10).{ client_id: 'play', name: 'OpenVibe.Play', redirect_uris: ['https://play.openvibe.network/auth/callback'] },// Private S3-compatible storage zones (ADR-031 amendment; contracts 0.173.0).@@@ -316,7 +316,7 @@ async function createFrameService(db, config, analytics, { privateKey = null, isfunction linkPool() {const { catalog } = toolsCatalog.peek();const pool = new Map();-- for (const s of SITES.filter(x => x.status === 'open')) pool.set('site:' + s.id, { name: fullName(s), url: `https://${s.host}/`, about: s.tagline });++ for (const s of SITES.filter(x => x.status === 'open')) pool.set('site:' + s.id, { name: fullName(s), url: s.url || `https://${s.host}/`, about: s.tagline });for (const f of catalog.families) if (f.path) pool.set('family:' + f.id, { name: f.name, url: 'https://openvibe.tools' + f.path, about: f.tagline });for (const t of catalog.tools) pool.set('tool:' + t.id, { name: t.name, url: t.url, about: t.tagline });return pool;@@@ -371,7 +371,7 @@ async function createFrameService(db, config, analytics, { privateKey = null, iscards: menuCards.cards(),groups: GROUPS.map(g => ({ id: g.id, name: g.name })),// state: 'internal' (runs on loopback, the domain still shows a placeholder) or 'placeholder' (planned).-- soon: SITES.filter(s => s.status === 'soon').map(s => ({ id: s.id, name: s.name, url: `https://${s.host}/`, icon: s.icon, state: s.state })),++ soon: SITES.filter(s => s.status === 'soon').map(s => ({ id: s.id, name: s.name, url: s.url || `https://${s.host}/`, icon: s.icon, state: s.state })),footer: { blurb: (ai && ai.blurb) || site.what, ai: !!ai, discover: footerDiscover, popular: popularTools(8), legal: { terms: legalBase + '/terms', privacy: legalBase + '/privacy', dmca: legalBase + '/dmca' } },};}@@@ -33,7 +33,7 @@ const ALL_SITES = RAW_SITES.map((s) => {const e = exposureOf(s.service);const host = manifestHost(s.service) || s.host;if (!host) throw new Error(`site ${s.id}: no host (its manifest has no publicOrigin and the entry names none)`);-- return { ...s, host, status: e.state === 'live' && e.public_site === 'service' ? 'open' : 'soon', state: e.state };++ return { ...s, host, ...(s.service === 'inventory' ? { url: 'https://openvibe.my/inventory' } : {}), status: e.state === 'live' && e.public_site === 'service' ? 'open' : 'soon', state: e.state };});// The menu, footer, home and llms.txt list SITES; siteForHost still knows Play so a Play-host page (or a token// consent screen) keeps its own name, without the site being offered as a destination.@@@ -76,7 +76,7 @@ const INTENTS = [['media-hub', 'fa-cloud-arrow-up', 'Keep and share files', 'A 25 GB drive with share links and folder backups.', 'https://openvibe.download/', 'download files drive upload share link storage backup sync cloud send mediafire mega'],['zone', 'fa-box-archive', 'Store files in buckets', 'Private S3-compatible storage zones for your projects, with keys you scope and quotas you set.', 'https://openvibe.zone/', 'storage s3 bucket buckets object storage files backup backups rclone aws zone data upload'],['play', 'fa-trophy', 'Climb the leaderboards', 'This week\'s leaders and the achievements you have unlocked in every OpenVibe game, now part of OpenVibe.Games.', 'https://openvibe.games/leaderboards', 'leaderboard leaderboards achievement achievements score scores ranking rank high score trophy games play'],-- ['inventory', 'fa-shirt', 'Wear what you earn', 'One inventory for the items you earn across OpenVibe, worn on every site.', 'https://inventory.openvibe.network/', 'inventory items cosmetics name effect particles hat voice badge collect equip wear profile'],++ ['inventory', 'fa-shirt', 'Wear what you earn', 'One inventory for the items you earn across OpenVibe, worn on every site.', 'https://openvibe.my/inventory', 'inventory items cosmetics name effect particles hat voice badge collect equip wear profile'],['watch', 'fa-bell', 'Get told when it changes', 'Watch a page, a feed or an API and hear when your value moves.', 'https://openvibe.watch/', 'watch monitor alert alerts notify change changes price drop feed rss tracker uptime'],['help', 'fa-life-ring', 'Get help', 'Every OpenVibe site\'s questions answered, and a person when they are not.', 'https://openvibe.help/', 'help support faq question questions ticket contact problem how'],];@@@ -25,7 +25,7 @@ const { siteForHost, fullName } = require('../frame/sites');const esc = seo.esc;const SITE = 'https://openvibe.network';--const INVENTORY_SITE = 'https://inventory.openvibe.network';++const INVENTORY_SITE = 'https://openvibe.my/inventory';const QUEST_SITE = 'https://openvibe.quest';const GAMES_ACHIEVEMENTS = 'https://openvibe.games/achievements#game-';const RECENT_ACHIEVEMENTS = 6; // per game on the page; the rest is on the game's row on OpenVibe.Games@@@ -43,7 +43,7 @@ const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July',const profileUrl = (username) => `${SITE}/@${encodeURIComponent(username)}`;const avatarUrl = (username, size = 160) => `${SITE}/avatar/${encodeURIComponent(username)}?s=${size}`;--const itemUrl = (definitionId) => `${INVENTORY_SITE}/items/${encodeURIComponent(definitionId)}`;++const itemUrl = (definitionId) => `${INVENTORY_SITE}/${encodeURIComponent(definitionId)}`;const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;const rarityRank = (r) => Math.max(0, RARITY.indexOf(r));@@@ -24,5 +24,14 @@ const { getDb, seedDb } = require('../server/db/database');const mail = await db.prepare('SELECT redirect_uris, is_first_party FROM oauth_clients WHERE client_id = ?').get('mail');assert.deepStrictEqual(JSON.parse(mail.redirect_uris), ['https://openvibe.email/auth/callback'], 'the only callback is openvibe.email');assert.strictEqual(mail.is_first_party, 1, 'the site client is first-party');++ const uris = async () => JSON.parse((await db.prepare('SELECT redirect_uris FROM oauth_clients WHERE client_id = ?').get('inventory')).redirect_uris);++ assert.ok((await uris()).includes('https://openvibe.my/inventory/auth/callback'));++ await db.prepare('UPDATE oauth_clients SET redirect_uris = ? WHERE client_id = ?')++ .run(JSON.stringify(['https://inventory.openvibe.network/auth/callback', 'https://inventory.openvibe.network/extra']), 'inventory');++ console.log = () => {};++ await seedDb(db, { log });++ console.log = quiet;++ assert.ok((await uris()).includes('https://openvibe.my/inventory/auth/callback'), 'existing clients gain the callback');++ assert.ok((await uris()).includes('https://inventory.openvibe.network/extra'), 'custom callbacks survive');console.log('client-names: ok');})().catch((err) => { console.error(err); process.exit(1); });@@@ -26,6 +26,13 @@ assert.ok(nginx.includes('proxy_set_header Host $host;'), 'the Host header is thassert.ok(nginx.includes('location ~* ^/(metrics|internal)(/|$)'), 'metrics and internal stay closed');assert.ok(!nginx.includes('return 302 https://openvibe.network/my;'), 'the old parked redirect is gone');++assert.ok(nginx.includes('location ^~ /inventory {'), 'Inventory has a dedicated prefix location');++assert.ok(nginx.includes('proxy_pass http://127.0.0.1:5030;'), 'the prefix is preserved for Inventory');++assert.ok(nginx.includes('^/inventory/(internal|metrics)(/|$)'), 'private Inventory endpoints stay closed');++const inventory = require('../server/frame/sites').SITES.find((s) => s.id === 'inventory');++assert.strictEqual(inventory.url, 'https://openvibe.my/inventory');++assert.ok(fs.readFileSync(path.join(ROOT, 'public/my.html'), 'utf8').includes('href="/inventory"'));++// ── It parses ──let tmp = null;try {@@@ -95,7 +95,7 @@ const INSTANCES = [assert.ok(r.text.includes('<span class="pf-count">4</span>'), 'four items');assert.ok(r.text.includes('"@type":"ProfilePage"') && r.text.includes('<link rel="canonical" href="https://openvibe.network/@Ana">'));assert.ok(r.text.includes('--pf-color:#ff0066'));-- assert.ok(r.text.includes('https://inventory.openvibe.network/items/itd_01JZ00000000000000000001R1'), 'items link to their Inventory page');++ assert.ok(r.text.includes('https://openvibe.my/inventory/itd_01JZ00000000000000000001R1'), 'items link to their Inventory page');assert.ok(!/name="robots" content="noindex"/.test(r.text));assert.ok(calls.filter((c) => !c.path.includes('/badges') && !c.path.endsWith('/achievements')).every((c) => c.auth === 'Bearer tok:openvibe.inventory:inventory.item.read'), 'Network\'s own token for Inventory');assert.strictEqual(calls.find((c) => c.path.endsWith('/achievements')).auth, '', 'Play\'s read is public: no token');@@@ -129,7 +129,7 @@ const INSTANCES = [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', 4, ['Rainbow', 'Royal Crown', 'OG Viewer']]);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://openvibe.my/inventory/u/${A}`);assert.deepStrictEqual(p.items.by_kind.map((k) => k.kind).sort(), ['live.hat', 'live.name_effect', 'live.particle', 'network.badge']);assert.deepStrictEqual(p.badges.map((b) => b.id), ['thread-starter', 'first-follow']);assert.deepStrictEqual([p.achievements.total, p.achievements.games.map((g) => [g.title, g.count])], [3, [['salvage-rush', 2], ['vibe-heroes', 1]]], 'achievements in the JSON every site reads');
@@ -114,6 +114,21 @@ server {
@@ -114,6 +114,21 @@ server {
location ~* ^/(metrics|internal)(/|$) { return 404; }
location ~* ^/(metrics|internal)(/|$) { return 404; }
+ # Inventory keeps its prefix: the app serves assets, forms and SSO under it.
+ # ^~ bypasses outer regex locations, so private endpoints are denied here too.
+ location ^~ /inventory {
+ if ($uri ~* "^/inventory/(internal|metrics)(/|$)") { return 404; }
+ if ($uri ~ "^/inventory/\\.(?!well-known)") { return 404; }
+ client_max_body_size 2m;
+ proxy_pass http://127.0.0.1:5030;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header CF-Connecting-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
# The hub's page, its sections, its /auth sign-in and every shared asset the page loads (css, js, shared,
# The hub's page, its sections, its /auth sign-in and every shared asset the page loads (css, js, shared,
# img, fonts, icons, manifest, service worker) are Network's own responses for this Host.
# img, fonts, icons, manifest, service worker) are Network's own responses for this Host.
location / {
location / {
@@ -33,19 +33,19 @@ body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMa
@@ -33,19 +33,19 @@ body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMa
/* Section tabs */
/* Section tabs */
.section-tabs{display:flex;gap:4px;margin-bottom:24px;background:var(--bg-secondary);border-radius:10px;padding:4px;overflow-x:auto}
.section-tabs{display:flex;gap:4px;margin-bottom:24px;background:var(--bg-secondary);border-radius:10px;padding:4px;overflow-x:auto}
-.section-tabs button{
- flex-shrink:0;padding:8px 16px;border:none;border-radius:8px;font-size:12px;font-weight:600;
+.section-tabs :is(button,a){
+ text-decoration:none;flex-shrink:0;padding:8px 16px;border:none;border-radius:8px;font-size:12px;font-weight:600;
background:transparent;color:var(--text-muted);cursor:pointer;transition:all var(--ov-dur-2, .15s);white-space:nowrap;
background:transparent;color:var(--text-muted);cursor:pointer;transition:all var(--ov-dur-2, .15s);white-space:nowrap;
}
}
.section-tabs button.active{background:var(--bg-card);color:var(--accent-light)}
.section-tabs button.active{background:var(--bg-card);color:var(--accent-light)}
-.section-tabs button:hover:not(.active){color:var(--text)}
+.section-tabs :is(button,a):hover:not(.active){color:var(--text)}
/* On a computer the tabs stand as a sidebar beside the open section (L43): every setting in view, the page shorter. */
/* On a computer the tabs stand as a sidebar beside the open section (L43): every setting in view, the page shorter. */
@media (min-width:1000px){
@media (min-width:1000px){
.page{max-width:1080px}
.page{max-width:1080px}
.settings{display:grid;grid-template-columns:208px minmax(0,1fr);gap:28px;align-items:start}
.settings{display:grid;grid-template-columns:208px minmax(0,1fr);gap:28px;align-items:start}
.settings .section-tabs{position:sticky;top:76px;flex-direction:column;gap:2px;margin:0;padding:6px;overflow:visible}
.settings .section-tabs{position:sticky;top:76px;flex-direction:column;gap:2px;margin:0;padding:6px;overflow:visible}
- .settings .section-tabs button{display:flex;align-items:center;gap:10px;width:100%;padding:10px 12px;font-size:13.5px;text-align:left}
- .settings .section-tabs button i{width:16px;text-align:center;opacity:.85}
+ .settings .section-tabs :is(button,a){display:flex;align-items:center;gap:10px;width:100%;padding:10px 12px;font-size:13.5px;text-align:left}
+ .settings .section-tabs :is(button,a) i{width:16px;text-align:center;opacity:.85}
.settings .section-tabs button.active{box-shadow:inset 3px 0 0 var(--accent)}
.settings .section-tabs button.active{box-shadow:inset 3px 0 0 var(--accent)}
}
}
@@ -142,6 +142,7 @@
@@ -142,6 +142,7 @@
<button onclick="showSection('apps',this)"><i class="fa-solid fa-plug"></i> Apps</button>
<button onclick="showSection('apps',this)"><i class="fa-solid fa-plug"></i> Apps</button>
<button onclick="showSection('linked',this)"><i class="fa-solid fa-link"></i> Linked Services</button>
<button onclick="showSection('linked',this)"><i class="fa-solid fa-link"></i> Linked Services</button>
<button onclick="showSection('machines',this)"><i class="fa-solid fa-laptop"></i> Machines</button>
<button onclick="showSection('machines',this)"><i class="fa-solid fa-laptop"></i> Machines</button>
+ <a href="/inventory"><i class="fa-solid fa-shirt"></i> Inventory</a>
<button onclick="showSection('security',this)"><i class="fa-solid fa-lock"></i> Security</button>
<button onclick="showSection('security',this)"><i class="fa-solid fa-lock"></i> Security</button>
<button onclick="showSection('anon',this)"><i class="fa-solid fa-user-secret"></i> Anon Info</button>
<button onclick="showSection('anon',this)"><i class="fa-solid fa-user-secret"></i> Anon Info</button>
</div>
</div>
@@ -73,7 +73,7 @@
@@ -73,7 +73,7 @@
<p class="w-amount" id="w-coins-amount">—</p>
<p class="w-amount" id="w-coins-amount">—</p>
<p class="w-line">Earned by watching and chatting</p>
<p class="w-line">Earned by watching and chatting</p>
<div class="w-actions">
<div class="w-actions">
- <a class="ovk-btn ovk-btn--quiet" href="https://inventory.openvibe.network/" target="_blank" rel="noopener">Spend your OpenCoins</a>
+ <a class="ovk-btn ovk-btn--quiet" href="https://openvibe.my/inventory" target="_blank" rel="noopener">Spend your OpenCoins</a>
</div>
</div>
</section>
</section>
</div>
</div>
@@ -173,7 +173,7 @@ async function seedDb(db, { log = console } = {}) {
@@ -173,7 +173,7 @@ async function seedDb(db, { log = console } = {}) {
// for uploads and albums (OpenVibe.Pics, 2026-10-15).
// for uploads and albums (OpenVibe.Pics, 2026-10-15).
{ client_id: 'media-hub', name: 'OpenVibe.Download', redirect_uris: ['https://openvibe.download/auth/callback', 'https://openvibe.video/auth/callback', 'https://openvibe.pics/auth/callback'] },
{ client_id: 'media-hub', name: 'OpenVibe.Download', redirect_uris: ['https://openvibe.download/auth/callback', 'https://openvibe.video/auth/callback', 'https://openvibe.pics/auth/callback'] },
// The network-wide inventory (ADR-054, plan T21).
// The network-wide inventory (ADR-054, plan T21).
- { client_id: 'inventory', name: 'OpenVibe.Inventory', redirect_uris: ['https://inventory.openvibe.network/auth/callback'] },
+ { client_id: 'inventory', name: 'OpenVibe.Inventory', redirect_uris: ['https://inventory.openvibe.network/auth/callback', 'https://openvibe.my/inventory/auth/callback'] },
// Achievements and leaderboards for every game (Games ADR-0007 decision 10).
// Achievements and leaderboards for every game (Games ADR-0007 decision 10).
{ client_id: 'play', name: 'OpenVibe.Play', redirect_uris: ['https://play.openvibe.network/auth/callback'] },
{ client_id: 'play', name: 'OpenVibe.Play', redirect_uris: ['https://play.openvibe.network/auth/callback'] },
// Private S3-compatible storage zones (ADR-031 amendment; contracts 0.173.0).
// Private S3-compatible storage zones (ADR-031 amendment; contracts 0.173.0).
@@ -316,7 +316,7 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
@@ -316,7 +316,7 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
function linkPool() {
function linkPool() {
const { catalog } = toolsCatalog.peek();
const { catalog } = toolsCatalog.peek();
const pool = new Map();
const pool = new Map();
- for (const s of SITES.filter(x => x.status === 'open')) pool.set('site:' + s.id, { name: fullName(s), url: `https://${s.host}/`, about: s.tagline });
+ for (const s of SITES.filter(x => x.status === 'open')) pool.set('site:' + s.id, { name: fullName(s), url: s.url || `https://${s.host}/`, about: s.tagline });
for (const f of catalog.families) if (f.path) pool.set('family:' + f.id, { name: f.name, url: 'https://openvibe.tools' + f.path, about: f.tagline });
for (const f of catalog.families) if (f.path) pool.set('family:' + f.id, { name: f.name, url: 'https://openvibe.tools' + f.path, about: f.tagline });
for (const t of catalog.tools) pool.set('tool:' + t.id, { name: t.name, url: t.url, about: t.tagline });
for (const t of catalog.tools) pool.set('tool:' + t.id, { name: t.name, url: t.url, about: t.tagline });
return pool;
return pool;
@@ -371,7 +371,7 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
@@ -371,7 +371,7 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
cards: menuCards.cards(),
cards: menuCards.cards(),
groups: GROUPS.map(g => ({ id: g.id, name: g.name })),
groups: GROUPS.map(g => ({ id: g.id, name: g.name })),
// state: 'internal' (runs on loopback, the domain still shows a placeholder) or 'placeholder' (planned).
// state: 'internal' (runs on loopback, the domain still shows a placeholder) or 'placeholder' (planned).
- soon: SITES.filter(s => s.status === 'soon').map(s => ({ id: s.id, name: s.name, url: `https://${s.host}/`, icon: s.icon, state: s.state })),
+ soon: SITES.filter(s => s.status === 'soon').map(s => ({ id: s.id, name: s.name, url: s.url || `https://${s.host}/`, icon: s.icon, state: s.state })),
footer: { blurb: (ai && ai.blurb) || site.what, ai: !!ai, discover: footerDiscover, popular: popularTools(8), legal: { terms: legalBase + '/terms', privacy: legalBase + '/privacy', dmca: legalBase + '/dmca' } },
footer: { blurb: (ai && ai.blurb) || site.what, ai: !!ai, discover: footerDiscover, popular: popularTools(8), legal: { terms: legalBase + '/terms', privacy: legalBase + '/privacy', dmca: legalBase + '/dmca' } },
};
};
}
}
@@ -33,7 +33,7 @@ const ALL_SITES = RAW_SITES.map((s) => {
@@ -33,7 +33,7 @@ const ALL_SITES = RAW_SITES.map((s) => {
const e = exposureOf(s.service);
const e = exposureOf(s.service);
const host = manifestHost(s.service) || s.host;
const host = manifestHost(s.service) || s.host;
if (!host) throw new Error(`site ${s.id}: no host (its manifest has no publicOrigin and the entry names none)`);
if (!host) throw new Error(`site ${s.id}: no host (its manifest has no publicOrigin and the entry names none)`);
- return { ...s, host, status: e.state === 'live' && e.public_site === 'service' ? 'open' : 'soon', state: e.state };
+ return { ...s, host, ...(s.service === 'inventory' ? { url: 'https://openvibe.my/inventory' } : {}), status: e.state === 'live' && e.public_site === 'service' ? 'open' : 'soon', state: e.state };
});
});
// The menu, footer, home and llms.txt list SITES; siteForHost still knows Play so a Play-host page (or a token
// The menu, footer, home and llms.txt list SITES; siteForHost still knows Play so a Play-host page (or a token
// consent screen) keeps its own name, without the site being offered as a destination.
// consent screen) keeps its own name, without the site being offered as a destination.
@@ -76,7 +76,7 @@ const INTENTS = [
@@ -76,7 +76,7 @@ const INTENTS = [
['media-hub', 'fa-cloud-arrow-up', 'Keep and share files', 'A 25 GB drive with share links and folder backups.', 'https://openvibe.download/', 'download files drive upload share link storage backup sync cloud send mediafire mega'],
['media-hub', 'fa-cloud-arrow-up', 'Keep and share files', 'A 25 GB drive with share links and folder backups.', 'https://openvibe.download/', 'download files drive upload share link storage backup sync cloud send mediafire mega'],
['zone', 'fa-box-archive', 'Store files in buckets', 'Private S3-compatible storage zones for your projects, with keys you scope and quotas you set.', 'https://openvibe.zone/', 'storage s3 bucket buckets object storage files backup backups rclone aws zone data upload'],
['zone', 'fa-box-archive', 'Store files in buckets', 'Private S3-compatible storage zones for your projects, with keys you scope and quotas you set.', 'https://openvibe.zone/', 'storage s3 bucket buckets object storage files backup backups rclone aws zone data upload'],
['play', 'fa-trophy', 'Climb the leaderboards', 'This week\'s leaders and the achievements you have unlocked in every OpenVibe game, now part of OpenVibe.Games.', 'https://openvibe.games/leaderboards', 'leaderboard leaderboards achievement achievements score scores ranking rank high score trophy games play'],
['play', 'fa-trophy', 'Climb the leaderboards', 'This week\'s leaders and the achievements you have unlocked in every OpenVibe game, now part of OpenVibe.Games.', 'https://openvibe.games/leaderboards', 'leaderboard leaderboards achievement achievements score scores ranking rank high score trophy games play'],
- ['inventory', 'fa-shirt', 'Wear what you earn', 'One inventory for the items you earn across OpenVibe, worn on every site.', 'https://inventory.openvibe.network/', 'inventory items cosmetics name effect particles hat voice badge collect equip wear profile'],
+ ['inventory', 'fa-shirt', 'Wear what you earn', 'One inventory for the items you earn across OpenVibe, worn on every site.', 'https://openvibe.my/inventory', 'inventory items cosmetics name effect particles hat voice badge collect equip wear profile'],
['watch', 'fa-bell', 'Get told when it changes', 'Watch a page, a feed or an API and hear when your value moves.', 'https://openvibe.watch/', 'watch monitor alert alerts notify change changes price drop feed rss tracker uptime'],
['watch', 'fa-bell', 'Get told when it changes', 'Watch a page, a feed or an API and hear when your value moves.', 'https://openvibe.watch/', 'watch monitor alert alerts notify change changes price drop feed rss tracker uptime'],
['help', 'fa-life-ring', 'Get help', 'Every OpenVibe site\'s questions answered, and a person when they are not.', 'https://openvibe.help/', 'help support faq question questions ticket contact problem how'],
['help', 'fa-life-ring', 'Get help', 'Every OpenVibe site\'s questions answered, and a person when they are not.', 'https://openvibe.help/', 'help support faq question questions ticket contact problem how'],
];
];
@@ -25,7 +25,7 @@ const { siteForHost, fullName } = require('../frame/sites');
@@ -25,7 +25,7 @@ const { siteForHost, fullName } = require('../frame/sites');
const esc = seo.esc;
const esc = seo.esc;
const SITE = 'https://openvibe.network';
const SITE = 'https://openvibe.network';
-const INVENTORY_SITE = 'https://inventory.openvibe.network';
+const INVENTORY_SITE = 'https://openvibe.my/inventory';
const QUEST_SITE = 'https://openvibe.quest';
const QUEST_SITE = 'https://openvibe.quest';
const GAMES_ACHIEVEMENTS = 'https://openvibe.games/achievements#game-';
const GAMES_ACHIEVEMENTS = 'https://openvibe.games/achievements#game-';
const RECENT_ACHIEVEMENTS = 6; // per game on the page; the rest is on the game's row on OpenVibe.Games
const RECENT_ACHIEVEMENTS = 6; // per game on the page; the rest is on the game's row on OpenVibe.Games
@@ -43,7 +43,7 @@ const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July',
@@ -43,7 +43,7 @@ const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July',
const profileUrl = (username) => `${SITE}/@${encodeURIComponent(username)}`;
const profileUrl = (username) => `${SITE}/@${encodeURIComponent(username)}`;
const avatarUrl = (username, size = 160) => `${SITE}/avatar/${encodeURIComponent(username)}?s=${size}`;
const avatarUrl = (username, size = 160) => `${SITE}/avatar/${encodeURIComponent(username)}?s=${size}`;
-const itemUrl = (definitionId) => `${INVENTORY_SITE}/items/${encodeURIComponent(definitionId)}`;
+const itemUrl = (definitionId) => `${INVENTORY_SITE}/${encodeURIComponent(definitionId)}`;
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
const rarityRank = (r) => Math.max(0, RARITY.indexOf(r));
const rarityRank = (r) => Math.max(0, RARITY.indexOf(r));
@@ -24,5 +24,14 @@ const { getDb, seedDb } = require('../server/db/database');
@@ -24,5 +24,14 @@ const { getDb, seedDb } = require('../server/db/database');
const mail = await db.prepare('SELECT redirect_uris, is_first_party FROM oauth_clients WHERE client_id = ?').get('mail');
const mail = await db.prepare('SELECT redirect_uris, is_first_party FROM oauth_clients WHERE client_id = ?').get('mail');
assert.deepStrictEqual(JSON.parse(mail.redirect_uris), ['https://openvibe.email/auth/callback'], 'the only callback is openvibe.email');
assert.deepStrictEqual(JSON.parse(mail.redirect_uris), ['https://openvibe.email/auth/callback'], 'the only callback is openvibe.email');
assert.strictEqual(mail.is_first_party, 1, 'the site client is first-party');
assert.strictEqual(mail.is_first_party, 1, 'the site client is first-party');
+ const uris = async () => JSON.parse((await db.prepare('SELECT redirect_uris FROM oauth_clients WHERE client_id = ?').get('inventory')).redirect_uris);
+ assert.ok((await uris()).includes('https://openvibe.my/inventory/auth/callback'));
+ await db.prepare('UPDATE oauth_clients SET redirect_uris = ? WHERE client_id = ?')
+ .run(JSON.stringify(['https://inventory.openvibe.network/auth/callback', 'https://inventory.openvibe.network/extra']), 'inventory');
+ console.log = () => {};
+ await seedDb(db, { log });
+ console.log = quiet;
+ assert.ok((await uris()).includes('https://openvibe.my/inventory/auth/callback'), 'existing clients gain the callback');
+ assert.ok((await uris()).includes('https://inventory.openvibe.network/extra'), 'custom callbacks survive');
console.log('client-names: ok');
console.log('client-names: ok');
})().catch((err) => { console.error(err); process.exit(1); });
})().catch((err) => { console.error(err); process.exit(1); });
@@ -26,6 +26,13 @@ assert.ok(nginx.includes('proxy_set_header Host $host;'), 'the Host header is th
@@ -26,6 +26,13 @@ assert.ok(nginx.includes('proxy_set_header Host $host;'), 'the Host header is th
assert.ok(nginx.includes('location ~* ^/(metrics|internal)(/|$)'), 'metrics and internal stay closed');
assert.ok(nginx.includes('location ~* ^/(metrics|internal)(/|$)'), 'metrics and internal stay closed');
assert.ok(!nginx.includes('return 302 https://openvibe.network/my;'), 'the old parked redirect is gone');
assert.ok(!nginx.includes('return 302 https://openvibe.network/my;'), 'the old parked redirect is gone');
+assert.ok(nginx.includes('location ^~ /inventory {'), 'Inventory has a dedicated prefix location');
+assert.ok(nginx.includes('proxy_pass http://127.0.0.1:5030;'), 'the prefix is preserved for Inventory');
+assert.ok(nginx.includes('^/inventory/(internal|metrics)(/|$)'), 'private Inventory endpoints stay closed');
+const inventory = require('../server/frame/sites').SITES.find((s) => s.id === 'inventory');
+assert.strictEqual(inventory.url, 'https://openvibe.my/inventory');
+assert.ok(fs.readFileSync(path.join(ROOT, 'public/my.html'), 'utf8').includes('href="/inventory"'));
+
// ── It parses ──
// ── It parses ──
let tmp = null;
let tmp = null;
try {
try {
@@ -95,7 +95,7 @@ const INSTANCES = [
@@ -95,7 +95,7 @@ const INSTANCES = [
assert.ok(r.text.includes('<span class="pf-count">4</span>'), 'four items');
assert.ok(r.text.includes('<span class="pf-count">4</span>'), 'four items');
assert.ok(r.text.includes('"@type":"ProfilePage"') && r.text.includes('<link rel="canonical" href="https://openvibe.network/@Ana">'));
assert.ok(r.text.includes('"@type":"ProfilePage"') && r.text.includes('<link rel="canonical" href="https://openvibe.network/@Ana">'));
assert.ok(r.text.includes('--pf-color:#ff0066'));
assert.ok(r.text.includes('--pf-color:#ff0066'));
- assert.ok(r.text.includes('https://inventory.openvibe.network/items/itd_01JZ00000000000000000001R1'), 'items link to their Inventory page');
+ assert.ok(r.text.includes('https://openvibe.my/inventory/itd_01JZ00000000000000000001R1'), 'items link to their Inventory page');
assert.ok(!/name="robots" content="noindex"/.test(r.text));
assert.ok(!/name="robots" content="noindex"/.test(r.text));
assert.ok(calls.filter((c) => !c.path.includes('/badges') && !c.path.endsWith('/achievements')).every((c) => c.auth === 'Bearer tok:openvibe.inventory:inventory.item.read'), 'Network\'s own token for Inventory');
assert.ok(calls.filter((c) => !c.path.includes('/badges') && !c.path.endsWith('/achievements')).every((c) => c.auth === 'Bearer tok:openvibe.inventory:inventory.item.read'), 'Network\'s own token for Inventory');
assert.strictEqual(calls.find((c) => c.path.endsWith('/achievements')).auth, '', 'Play\'s read is public: no token');
assert.strictEqual(calls.find((c) => c.path.endsWith('/achievements')).auth, '', 'Play\'s read is public: no token');
@@ -129,7 +129,7 @@ const INSTANCES = [
@@ -129,7 +129,7 @@ const INSTANCES = [
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', 4, ['Rainbow', 'Royal Crown', 'OG Viewer']]);
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', 4, ['Rainbow', 'Royal Crown', 'OG Viewer']]);
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://openvibe.my/inventory/u/${A}`);
assert.deepStrictEqual(p.items.by_kind.map((k) => k.kind).sort(), ['live.hat', 'live.name_effect', 'live.particle', 'network.badge']);
assert.deepStrictEqual(p.items.by_kind.map((k) => k.kind).sort(), ['live.hat', 'live.name_effect', 'live.particle', 'network.badge']);
assert.deepStrictEqual(p.badges.map((b) => b.id), ['thread-starter', 'first-follow']);
assert.deepStrictEqual(p.badges.map((b) => b.id), ['thread-starter', 'first-follow']);
assert.deepStrictEqual([p.achievements.total, p.achievements.games.map((g) => [g.title, g.count])], [3, [['salvage-rush', 2], ['vibe-heroes', 1]]], 'achievements in the JSON every site reads');
assert.deepStrictEqual([p.achievements.total, p.achievements.games.map((g) => [g.title, g.count])], [3, [['salvage-rush', 2], ['vibe-heroes', 1]]], 'achievements in the JSON every site reads');