openvibers/openvibe.cam Commits

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

OP OpenVibers committed 11 days ago · 3e18063b78 · 4 files, +99 −0

* Agent layer: /.well-known/agent.json and Markdown twins of the home, camera app and developers pages

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

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

Changes

ddiff --git a/server/agent-descriptor.js b/server/agent-descriptor.js
nnew file mode 100644
iindex 0000000..5c07344
---- /dev/null
++++ b/server/agent-descriptor.js
@@@ -0,0 +1,17 @@
++'use strict';
++// What openvibe.cam 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 /developers page that documents it,
++// and the llms.txt/sitemap machine map. The API authenticates a person's cam_at session or Network user
++// token and needs no app or service token; no OpenAPI document is published, so none is named.
++module.exports = {
++ id: 'cam',
++ name: 'OpenVibe.Cam',
++ purpose: 'The OpenVibe camera app: on-device filters plus a timer, burst and grid, private cameras with revocable viewer grants, stills from live streams, and the API behind Live\'s cam button.',
++ home: 'https://openvibe.cam',
++ site: 'openvibe.cam',
++ api: '/api/v1',
++ docs: '/developers',
++ policy: {
++ auth: ['none', 'Network OIDC'],
++ },
++};
ddiff --git a/server/agent-pages.js b/server/agent-pages.js
nnew file mode 100644
iindex 0000000..af2cff2
---- /dev/null
++++ b/server/agent-pages.js
@@@ -0,0 +1,57 @@
++'use strict';
++// The Markdown twins of openvibe.cam's public pages (openvibe-shared/agent-layer), rendered by the same
++// view builders and page shell the routes use (views/index.js through render/layout.js's renderPage),
++// signed out so a twin can never carry a signed-in person's photos or a private wall. Only the pages the
++// site itself marks indexable are twinned: the home, the camera app's signed-out shell and /developers.
++// /my, /w/:id, /s/:id and every per-person page are never rendered here.
++const seo = require('openvibe-shared/seo');
++const { renderPage } = require('./render/layout');
++const { homeJsonLd } = require('./http/discovery');
++const { raw } = require('./render/html');
++const views = require('./views');
++
++function createAgentPages({ config }) {
++ const twin = (path, o) => ({ html: renderPage({ ...o, path, config, viewer: { kind: 'anonymous' } }) });
++ const base = () => String(config.baseUrl).replace(/\/+$/, '');
++
++ function home() {
++ return twin('/', {
++ index: true, title: null,
++ description: 'OpenVibe.Cam is the camera app: on-device filters, private cameras with revocable viewer grants, and a still from a Live stream for its chat.',
++ jsonLd: homeJsonLd(config),
++ body: raw(views.home({ signedIn: false })),
++ });
++ }
++
++ function app() {
++ return twin('/app', {
++ index: true, title: 'The camera app',
++ description: 'Take a photo with on-device colour, light and blur, a timer, a burst and a grid; save it to OpenVibe.Pics.',
++ body: raw(views.app({ signedIn: false, photos: [] })),
++ });
++ }
++
++ function developers() {
++ return twin('/developers', {
++ index: true, title: 'The OpenVibe.Cam API',
++ description: 'The JSON API, the viewer-grant rules and the Live cam button contract.',
++ jsonLd: [seo.jsonLd.webPage({
++ name: 'The OpenVibe.Cam API', url: `${base()}/developers`,
++ description: 'The JSON API, the viewer-grant rules and the Live cam button contract.', siteUrl: base(),
++ })],
++ body: raw(views.developers()),
++ });
++ }
++
++ async function renderPublicPage(path) {
++ const p = String(path || '/').replace(/\/+$/, '') || '/';
++ if (p === '/') return home();
++ if (p === '/app') return app();
++ if (p === '/developers') return developers();
++ return null;
++ }
++
++ return { renderPublicPage };
++}
++
++module.exports = { createAgentPages };
ddiff --git a/server/app.js b/server/app.js
iindex b4e95b0..1f21779 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -148,6 +148,20 @@ async function createApp(opts = {}) {
// ── Pages ───────────────────────────────────────────────
app.use(rateLimit({ windowMs: 60_000, limit: Number(process.env.CAM_RATE_LIMIT_PER_MIN) || 300, standardHeaders: true, legacyHeaders: false }));
++ // The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json and the Markdown twins of the
++ // indexable public pages (server/agent-pages.js). The pages keep their own routes — a signed-in person's
++ // photos, an owner's camera and a private wall are theirs — so only the manifest and twin requests
++ // reach this mount; the pages advertise the twin themselves (render/layout.js).
++ const agentLayer = express();
++ require('openvibe-shared/agent-layer').mount(agentLayer, {
++ descriptor: require('./agent-descriptor'),
++ render: require('./agent-pages').createAgentPages({ config: ctx.config }).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(createPageRoutes(ctx));
app.use((req, res) => send(res, 404, {
viewer: req.viewer, config, path: req.originalUrl, title: 'Not found',
ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex d1a4189..23eff90 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -47,6 +47,11 @@ function renderPage(o) {
const pathNow = o.path || '/';
const loginNext = encodeURIComponent(pathNow);
const canonical = `${o.config.baseUrl}${pathNow.split('?')[0]}`;
++ // An indexable page has a Markdown twin (server/agent-pages.js) and names it in the head; a page drawn
++ // for a signed-in person, or a private wall/still, is noindex and advertises none.
++ const agentHead = o.index
++ ? require('openvibe-shared/agent-layer').linkHeaderTags(canonical, `${String(o.config.baseUrl).replace(/\/+$/, '')}/llms.txt`).head
++ : '';
const nav = {
service: 'cam',
apiBase: NETWORK_URL,
@@@ -81,6 +86,7 @@ function renderPage(o) {
navbar: nav,
footer,
head: `<meta name="referrer" content="strict-origin-when-cross-origin">
++${agentHead}
${appIcon.headTags({ site: 'cam' })}
<link rel="stylesheet" href="${ovServe.url('kit.css')}">
<link rel="stylesheet" href="${ovServe.url('room.css')}">
@@@ -112,6 +118,11 @@ function send(res, status, o) {
const personal = o.viewer && o.viewer.kind === 'user';
if (!res.get('Cache-Control')) res.set('Cache-Control', personal ? cache.htmlHeaders({ private: true }) : (o.cache || cache.htmlHeaders({ private: true })));
res.set('Vary', 'Cookie');
++ // An indexable page names its Markdown twin in the Link header, exactly as the agent layer's own responses do.
++ if (o.index) {
++ const canonical = `${String(o.config.baseUrl).replace(/\/+$/, '')}${String(o.path || '/').split('?')[0]}`;
++ res.set('Link', require('openvibe-shared/agent-layer').linkHeaderTags(canonical, `${String(o.config.baseUrl).replace(/\/+$/, '')}/llms.txt`).header);
++ }
res.status(status).type('html').send(renderPage(o));
}