openvibers/openvibe.network Commits

Move Inventory navigation and proxy routing into openvibe.my (#666)

OP OpenVibers committed 5 days ago · 3e387b1410 · 12 files, +47 −15

Changes

ddiff --git a/deploy/nginx/parked-domains.conf b/deploy/nginx/parked-domains.conf
iindex 49d13c50..5422f6ca 100644
---- a/deploy/nginx/parked-domains.conf
++++ b/deploy/nginx/parked-domains.conf
@@@ -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 / {
ddiff --git a/public/css/my.css b/public/css/my.css
iindex 304342e1..6798ef6d 100644
---- a/public/css/my.css
++++ b/public/css/my.css
@@@ -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)}
}
ddiff --git a/public/my.html b/public/my.html
iindex 64d0fdae..5809aa44 100644
---- a/public/my.html
++++ b/public/my.html
@@@ -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>
ddiff --git a/public/wallet.html b/public/wallet.html
iindex a9f342cb..35434e77 100644
---- a/public/wallet.html
++++ b/public/wallet.html
@@@ -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>
ddiff --git a/server/db/database.js b/server/db/database.js
iindex 232b961a..344730eb 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -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).
ddiff --git a/server/frame/service.js b/server/frame/service.js
iindex 257a063b..3cc53f50 100644
---- a/server/frame/service.js
++++ b/server/frame/service.js
@@@ -316,7 +316,7 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
function 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, is
cards: 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' } },
};
}
ddiff --git a/server/frame/sites.js b/server/frame/sites.js
iindex 84b89055..ca13c151 100644
---- a/server/frame/sites.js
++++ b/server/frame/sites.js
@@@ -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.
ddiff --git a/server/home/render.js b/server/home/render.js
iindex b231acaa..32be80d5 100644
---- a/server/home/render.js
++++ b/server/home/render.js
@@@ -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'],
];
ddiff --git a/server/profile/public.js b/server/profile/public.js
iindex 54208450..c917961f 100644
---- a/server/profile/public.js
++++ b/server/profile/public.js
@@@ -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));
ddiff --git a/test/client-names.test.js b/test/client-names.test.js
iindex ee275dfa..7d423710 100644
---- a/test/client-names.test.js
++++ b/test/client-names.test.js
@@@ -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); });
ddiff --git a/test/my-vhost.test.js b/test/my-vhost.test.js
iindex c8200d97..bd8dd48c 100644
---- a/test/my-vhost.test.js
++++ b/test/my-vhost.test.js
@@@ -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('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 {
ddiff --git a/test/public-profile.test.js b/test/public-profile.test.js
iindex 81badbcf..07343c5b 100644
---- a/test/public-profile.test.js
++++ b/test/public-profile.test.js
@@@ -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');