openvibers/openvibe.vip Commits

Agent layer: /.well-known/agent.json and Markdown twins of the public pages (#115)

OP OpenVibers committed 11 days ago · 41f7ce3cbb · 5 files, +117 −3

* Agent layer: /.well-known/agent.json and Markdown twins of the home, updates and creator pages

* Merge remote-tracking branch 'origin/main' into refresh-115

# Conflicts:
#	README.md
#	STATUS.json
#	package-lock.json
#	package.json

* Merge remote-tracking branch 'origin/main' into refresh-115

Changes

ddiff --git a/server/agent-descriptor.js b/server/agent-descriptor.js
nnew file mode 100644
iindex 0000000..19ca924
---- /dev/null
++++ b/server/agent-descriptor.js
@@@ -0,0 +1,16 @@
++'use strict';
++// What openvibe.vip tells an agent about itself (openvibe-shared/agent-layer): /.well-known/agent.json.
++// Every field is something this site really serves: the JSON API, the llms.txt/llms-full.txt/sitemap map,
++// and the auth that API accepts (service client-credentials tokens and Network user tokens). No OpenAPI
++// document or documentation page is published, so neither is named.
++module.exports = {
++ id: 'vip',
++ name: 'OpenVibe.VIP',
++ purpose: 'Memberships across the OpenVibe network: creators publish plans and versioned terms, members join through OpenVibe.Billing, and perks and gated resources follow the membership.',
++ home: 'https://openvibe.vip',
++ site: 'openvibe.vip',
++ api: '/api/v1',
++ policy: {
++ auth: ['none', 'Network OIDC', 'service token'],
++ },
++};
ddiff --git a/server/agent-pages.js b/server/agent-pages.js
nnew file mode 100644
iindex 0000000..fb1f47b
---- /dev/null
++++ b/server/agent-pages.js
@@@ -0,0 +1,69 @@
++'use strict';
++// The Markdown twins of openvibe.vip' public pages (openvibe-shared/agent-layer), rendered by the same
++// public data readers and page builders the routes use (web/pages.js through web/layout.js). They are
++// signed out, so no member state, join form token or owner view can leak into a twin. Private pages
++// (/me, /dashboard, auth, API) are never rendered here, and a creator page with no published plan is
++// noindex on the site, so it has no twin either.
++const pages = require('./web/pages');
++
++const RESERVED = new Set(['me', 'dashboard', 'auth', 'api', 'internal', 'css', 'js', 'terms', 'privacy', 'dmca', 'robots.txt', 'sitemap.xml', 'llms.txt', 'llms-full.txt', 'release.json', 'metrics', 'favicon.ico', 'embed']);
++
++function createAgentPages({ domain, config, layout, liveNow }) {
++ const { creators, plans, prices, billing } = domain;
++
++ async function home() {
++ const list = (await creators.listPublic()).map(creators.present);
++ const live = liveNow ? await liveNow.list() : [];
++ return {
++ html: layout.page({
++ title: pages.HOME_TITLE, description: pages.HOME_DESCRIPTION, jsonLd: [pages.homeFaqJsonLd()],
++ active: 'home', canonicalPath: '/', styles: [require('openvibe-shared/showcase').STYLESHEET], viewer: null,
++ body: pages.home({ creators: list, live, networkUrl: config.network.url }),
++ }),
++ };
++ }
++
++ function updates() {
++ return {
++ html: layout.page({
++ canonicalPath: '/updates', title: 'What shipped on OpenVibe.VIP', viewer: null,
++ body: require('openvibe-shared/frame').updatesBody({ service: 'vip', siteName: 'OpenVibe.VIP' }) + `<script src="${require('openvibe-shared/serve').url('shipped.js')}" defer></script>`,
++ }),
++ };
++ }
++
++ async function creator(username) {
++ if (RESERVED.has(String(username).toLowerCase())) return null;
++ const c = await creators.byUsername(username);
++ if (!c || c.status !== 'active') return null;
++ const list = await prices.withPrices(c.subject, await Promise.all((await plans.list({ creatorId: c.id })).map(async (p) => await plans.present(p))));
++ if (!list.length) return null; // the site keeps a planless creator page noindex
++ // The price line reads Billing's published charge; a Billing outage leaves the page's honest fallback.
++ let chargeRates = null;
++ if (list.some((p) => p.purchasable)) { try { chargeRates = await billing.rates(); } catch { chargeRates = null; } }
++ const name = c.display_name || c.username;
++ return {
++ html: layout.page({
++ title: `${name} memberships`, active: 'creator', canonicalPath: `/${c.username}`, robots: 'index,follow',
++ description: c.bio || `Membership plans from ${name} on OpenVibe.`, viewer: null,
++ body: pages.creatorPage({
++ creator: creators.present(c), plans: list, viewer: null, member: null, rates: chargeRates,
++ providers: config.billing.providers, token: '', query: {}, isOwner: false,
++ }),
++ }),
++ };
++ }
++
++ async function renderPublicPage(path) {
++ const p = String(path || '/').replace(/\/+$/, '') || '/';
++ if (p === '/') return home();
++ if (p === '/updates') return updates();
++ const m = /^\/([^/]+)$/.exec(p);
++ if (!m) return null;
++ return creator(decodeURIComponent(m[1]));
++ }
++
++ return { renderPublicPage };
++}
++
++module.exports = { createAgentPages };
ddiff --git a/server/app.js b/server/app.js
iindex c958a6d..ce3c90c 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -136,7 +136,22 @@ async function createApp(opts = {}) {
},
}));
const liveNow = opts.liveNow || require('./live-now').createLiveNow({ config, fetchImpl, log });
-- app.use(createWebRoutes({ domain, config, layout, userAuth, liveNow }));
++ // The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json and the Markdown twins of the
++ // public pages (server/agent-pages.js). The pages keep their own routes — a signed-in viewer's member
++ // state, join form and anti-forgery token are theirs — so only the manifest and twin requests reach
++ // this mount; the pages advertise the twin themselves (web/layout.js and web/routes.js).
++ const webRoutes = createWebRoutes({ domain, config, layout, userAuth, liveNow });
++ const agentLayer = express();
++ require('openvibe-shared/agent-layer').mount(agentLayer, {
++ descriptor: require('./agent-descriptor'),
++ render: require('./agent-pages').createAgentPages({ domain, config, layout, liveNow }).renderPublicPage,
++ });
++ app.use((req, res, next) => {
++ const twin = req.path.endsWith('.md') || (req.query && req.query.format === 'markdown') || String(req.headers.accept || '').includes('text/markdown');
++ if (req.path !== '/.well-known/agent.json' && !twin) return next();
++ return agentLayer(req, res, next);
++ });
++ app.use(webRoutes);
app.use((req, res) => {
if (req.path.startsWith('/api/') || req.path.startsWith('/internal/')) return http.sendProblem(res, 404, 'not_found', { ctx: req.ov });
ddiff --git a/server/web/layout.js b/server/web/layout.js
iindex 1b2149c..fcb2f43 100644
---- a/server/web/layout.js
++++ b/server/web/layout.js
@@@ -63,6 +63,12 @@ function createLayout({ config, release }) {
notificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)
};
const footer = { service: 'vip', variant: 'auto', path: o.canonicalPath, mount: '#ov-footer', brandName: SITE_NAME, updates: '/updates' }; // full on the front page only (Shared 4.19)
++ const robots = o.robots || 'index,follow';
++ // Every indexable page has a Markdown twin (server/agent-pages.js) and names it in the head. A page
++ // rendered for a signed-in person, or a planless creator page, is noindex and advertises none.
++ const agentHead = o.canonicalPath && !String(robots).includes('noindex')
++ ? require('openvibe-shared/agent-layer').linkHeaderTags(canonical, `${String(config.baseUrl).replace(/\/+$/, '')}/llms.txt`).head
++ : '';
const who = o.viewer
? `<span class="who">Signed in as <b>${esc(o.viewer.name || o.viewer.username || 'you')}</b> · <a href="/auth/logout?next=/">Sign out</a></span>`
: `<a class="who" href="/auth/login?next=${encodeURIComponent(o.canonicalPath || '/')}">Sign in</a>`;
@@@ -73,12 +79,13 @@ function createLayout({ config, release }) {
name: SITE_NAME, service: 'vip', lang: 'en',
title: o.title || `${SITE_NAME} — memberships across OpenVibe`,
titleSuffix: o.title ? ` · ${SITE_NAME}` : undefined,
-- siteName: SITE_NAME, description, canonical, robots: o.robots || 'index,follow',
++ siteName: SITE_NAME, description, canonical, robots,
type: o.ogType || 'website', image: o.ogImage, jsonLd: o.jsonLd,
summary: description, url: canonical,
navbar: nav, footer, home: '/', navLinks: NAV_LINKS,
head: [
'<meta name="referrer" content="strict-origin-when-cross-origin">',
++ agentHead,
appIcon.headTags({ site: 'vip' }),
release ? release.metaTag() : '',
`<link rel="stylesheet" href="${asset('css/vip.css')}">`,
ddiff --git a/server/web/routes.js b/server/web/routes.js
iindex 01028b2..6a6a378 100644
---- a/server/web/routes.js
++++ b/server/web/routes.js
@@@ -51,7 +51,14 @@ function createWebRoutes({ domain, config, layout, userAuth, liveNow }) {
return ratesCache.value;
}
-- const send = (res, status, o) => res.status(status).type('html').send(layout.page(o));
++ const send = (res, status, o) => {
++ // A public page names its Markdown twin in the Link header, exactly as the agent layer's own
++ // responses do; a page rendered for a signed-in person (noindex) has no twin and no header.
++ if (o && o.canonicalPath && !String(o.robots || 'index,follow').includes('noindex')) {
++ res.set('Link', require('openvibe-shared/agent-layer').linkHeaderTags(layout.abs(o.canonicalPath), `${String(config.baseUrl).replace(/\/+$/, '')}/llms.txt`).header);
++ }
++ return res.status(status).type('html').send(layout.page(o));
++ };
const dollars = (cents) => `$${(cents / 100).toFixed(2)}`;
/** A price typed in a form ("4.99", "$5"): cents, or null when left blank; anything else is refused. */
function priceInput(v, { required = false } = {}) {