openvibers/openvibe.mediahub Commits

openvibe-shared v4.108.0 (was v4.107.4) (#283)

OP OpenVibers committed 10 days ago · dc312366ca · 10 files, +56 −23

* openvibe-shared v4.108.0 (was v4.107.4)

Opened by scripts/pin-bump.js in OpenVibers/OpenVibe.Shared after the v4.108.0 release.

* llms.txt links each page at its Markdown twin, with an ## Optional section (AGT-5)

Changes

ddiff --git a/README.md b/README.md
iindex 23ba763..e32b167 100644
---- a/README.md
++++ b/README.md
@@@ -525,5 +525,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
- openvibe-contracts: v0.222.0
- openvibe-sdk: v0.82.0
--- openvibe-shared: v4.107.4
++- openvibe-shared: v4.108.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex 49a77ba..9358f3f 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -10,7 +10,7 @@
"packages": [
"openvibe-contracts v0.222.0",
"openvibe-sdk v0.82.0",
-- "openvibe-shared v4.107.4"
++ "openvibe-shared v4.108.0"
],
"summary": "OpenVibe.MediaHub: openvibe.download as a private drive. Account-only uploads stream into OpenVibe.Media (a plain form up to 256 MB, resumable parts beyond). Files are private. Share links expire in 1 hour to 7 days, need a signed-in downloader and can be limited to named accounts (matched by subject). Reports suspend a share for staff. Downloads are always sandboxed attachments. Per-person quotas, and account export and deletion (ADR-033). openvibe.video serves public watch pages and playlists for Live's recordings and clips (no uploads); openvibe.pics is parked and redirects to openvibe.network.",
"works": [
@@@ -44,7 +44,7 @@
],
"contracts": "openvibe-contracts v0.222.0",
"sdk": "openvibe-sdk v0.82.0",
-- "shared": "openvibe-shared v4.107.4",
++ "shared": "openvibe-shared v4.108.0",
"deployment": "openvibe-ovh: unit openvibe-media-hub.service on 127.0.0.1:4990 (git checkout /opt/openvibe.download, env /etc/openvibe/media-hub.env, database ov_media_hub), deployed with `sudo ovhost deploy media-hub`; public at https://openvibe.download since 2026-10-08, https://openvibe.video since 2026-10-12 and https://openvibe.pics since 2026-10-15.",
"updated": "2026-11-03"
}
ddiff --git a/package-lock.json b/package-lock.json
iindex 1e23d77..c20925b 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -17,7 +17,7 @@
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0",
"pg": "^8.23.0",
"sharp": "^0.33.5"
},
@@@ -1654,9 +1654,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.107.4",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",
-- "integrity": "sha512-tThN0m3KMncEHVcBOvE+ysHjsZTlVIDLG1P+/i2Mulmxfc/AUpebAXhpWWrut50OChZ8o+HeePxsSrPI8mUQOQ==",
++ "version": "4.108.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0",
++ "integrity": "sha512-NPzwjxpzGIpFpIm4uw3+VwHfsxbJ2+cD9yuhRzSwolSwqv2kCmwaVtHJ0DA9gKO3K8PCBfIP3UpKzpuSbYEJ+g==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/package.json b/package.json
iindex 5c936bc..97ce508 100644
---- a/package.json
++++ b/package.json
@@@ -24,7 +24,7 @@
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0",
"pg": "^8.23.0",
"sharp": "^0.33.5"
},
ddiff --git a/server/http/discovery.js b/server/http/discovery.js
iindex 761b7a7..1febf6a 100644
---- a/server/http/discovery.js
++++ b/server/http/discovery.js
@@@ -79,6 +79,10 @@ function createDiscoveryRoutes(ctx) {
r.get('/llms.txt', (_req, res) => {
res.type('text/plain').set('Cache-Control', TEXT).send(seo.llmsTxt({
name: SITE_NAME,
++ // AGT-5: each of Download's own pages is linked at its Markdown twin (openvibe-shared 4.108.0 leaves files
++ // and other sites' links as they are). What an agent may skip is in `optional`, written last.
++ markdown: true,
++ base: site,
summary: 'OpenVibe.Download: a private drive on your OpenVibe account, with share links you control and folder backups.',
details: 'Upload files to your own drive, share a file or folder with named OpenVibe accounts, anyone signed in, or a public download page (links expire and can be revoked), and keep a folder on your computer backed up with the openvibe-download command (a sync key from /sync). Every page is server-rendered and readable without JavaScript.',
sections: [
@@@ -88,14 +92,15 @@ function createDiscoveryRoutes(ctx) {
{ title: 'What shipped on OpenVibe.MediaHub', url: abs('/updates') },
] },
{ title: 'Machine-readable', links: [
-- { title: 'Sitemap', url: abs('/sitemap.xml') },
-- { title: 'Full text for language models', url: abs('/llms-full.txt') },
{ title: 'Release metadata (JSON)', url: abs('/release.json') },
] },
-- { title: 'Elsewhere', links: [
-- { title: 'OpenVibe.Network', url: 'https://openvibe.network', note: 'accounts, apps and grants' },
-- { title: 'OpenVibe.Services', url: 'https://openvibe.services', note: 'apps, keys and capability grants' },
-- ] },
++ ],
++ // llmstxt.org's "## Optional": what an agent may skip when it needs a shorter context.
++ optional: [
++ { title: 'Sitemap', url: abs('/sitemap.xml') },
++ { title: 'Full text for language models', url: abs('/llms-full.txt') },
++ { title: 'OpenVibe.Network', url: 'https://openvibe.network', note: 'accounts, apps and grants' },
++ { title: 'OpenVibe.Services', url: 'https://openvibe.services', note: 'apps, keys and capability grants' },
],
}));
});
ddiff --git a/server/pics/pages.js b/server/pics/pages.js
iindex 3fe9c6d..6d6d7f9 100644
---- a/server/pics/pages.js
++++ b/server/pics/pages.js
@@@ -627,18 +627,26 @@ ${row.state === 'hidden' ? html`<button class="ovp-btn" name="action" value="res
.send(seo.sitemapXml(['/', '/updates', ...legal.PATHS, ...POLICY_PATHS].map((p) => ({ loc: `${base}${p}` })))));
// RFC 9116, canonical to this host (openvibe.pics).
r.get('/.well-known/security.txt', securityTxt.handler(base));
-- // The site as llms.txt describes it, shared by /llms.txt and /llms-full.txt (SEO-33).
++ // The site as llms.txt describes it, shared by /llms.txt and /llms-full.txt (SEO-33). AGT-5: its own pages are
++ // linked at their Markdown twins; `base` is this host's origin, so other sites' links keep theirs.
const llmsSite = () => ({
name: SITE_NAME,
++ markdown: true,
++ base,
summary: 'Picture hosting on the OpenVibe network: upload a picture, get a link to share or embed, albums, any size or format.',
details: 'Pictures are unlisted (reachable only by their link). Each one is re-encoded with its metadata removed, and can be had resized and converted at /i/<slug>/w/<320|640|1280|1920|2560>.<webp|avif|jpg|png>. Alt text: the uploader writes it or asks OpenVibe.AI for a draft (POST /api/v1/images/<slug>/describe). API: POST /api/v1/images with the picture as the body (an OpenVibe sign-in token as a Bearer).',
sections: [
-- { title: 'Start here', links: [{ title: SITE_NAME, url: `${base}/` }, { title: 'Terms', url: `${base}/terms` }, { title: 'DMCA', url: `${base}/dmca` }] },
-- { title: 'Policies', links: [
-- { title: 'What happens to your picture', url: `${base}/exif` },
-- { title: 'What you may put here', url: `${base}/content-policy` },
-- { title: 'Pictures and AI', url: `${base}/ai-training` },
-- ] },
++ { title: 'Start here', links: [{ title: SITE_NAME, url: `${base}/` }] },
++ ],
++ // llmstxt.org's "## Optional": what an agent may skip when it needs a shorter context.
++ optional: [
++ { title: 'What happens to your picture', url: `${base}/exif` },
++ { title: 'What you may put here', url: `${base}/content-policy` },
++ { title: 'Pictures and AI', url: `${base}/ai-training` },
++ { title: 'Terms', url: `${base}/terms` },
++ { title: 'DMCA', url: `${base}/dmca` },
++ { title: 'Sitemap', url: `${base}/sitemap.xml`, note: 'only the home, the update log and the policy pages: no picture or album is ever listed' },
++ { title: 'Full text for language models', url: `${base}/llms-full.txt` },
],
});
// /.well-known/agent.json on the Pics host (SEO-2): its own manifest, not the drive's or the video host's.
@@@ -653,6 +661,8 @@ ${row.state === 'hidden' ? html`<button class="ovp-btn" name="action" value="res
sections: [
{ title: 'About', pages: [{ title: d.name, url: `${base}/`, text: [d.summary, Array.isArray(d.details) ? d.details.join('\n') : d.details].filter(Boolean).join('\n\n') }] },
...(d.sections || []).map((x) => ({ title: x.title, pages: (x.links || []).map((l) => ({ title: l.title, url: l.url, text: l.note || '' })) })),
++ // AGT-5: the Optional links are still pages, so the full document names them too.
++ ...((d.optional || []).length ? [{ title: 'Optional', pages: d.optional.map((l) => ({ title: l.title, url: l.url, text: l.note || '' })) }] : []),
],
}));
});
ddiff --git a/server/video/pages.js b/server/video/pages.js
iindex f259753..79c6748 100644
---- a/server/video/pages.js
++++ b/server/video/pages.js
@@@ -1863,6 +1863,10 @@ ${isOwner ? html`<p class="ovv-fine">You can reorder items with the up and down
const links = (items, kind) => items.map((it) => ({ title: it.title || (kind === 'vod' ? 'Recording' : 'Clip'), url: `${base}/${kind === 'vod' ? 'v' : 'c'}/${it.id}`, note: String(it.description || it.ai_summary || it.summary || '').replace(/\s+/g, ' ').trim().slice(0, 600) }));
return {
name: SITE_NAME,
++ // AGT-5: each of Video's own pages is linked at its Markdown twin (openvibe-shared 4.108.0 leaves files and
++ // other sites' links as they are). Both /llms.txt and /llms-full.txt read this object.
++ markdown: true,
++ base,
summary: 'Videos people upload or record in the browser, and stream recordings and clips from OpenVibe.Live, each with a watch page.',
details: 'AI captions, chapters and a summary are made for uploads (OpenVibe.AI, on OpenVibe\'s own servers) and rendered on the watch pages, as are the AI chapters and captions of stream recordings. The AI cut makes an edited copy of an upload: the pauses cut out, or a highlights reel. Uploading needs a signed-in OpenVibe account; /upload and /record.',
sections: [
@@@ -1870,6 +1874,11 @@ ${isOwner ? html`<p class="ovv-fine">You can reorder items with the up and down
{ title: 'Latest recordings', links: links(vodItems, 'vod') },
{ title: 'Latest clips', links: links(clipItems, 'clip') },
],
++ // llmstxt.org's "## Optional": what an agent may skip when it needs a shorter context.
++ optional: [
++ { title: 'Sitemap', url: `${base}/sitemap.xml`, note: 'the watch pages Video is the canonical home of' },
++ { title: 'Full text for language models', url: `${base}/llms-full.txt` },
++ ],
};
}
r.get('/llms.txt', async (req, res) => {
ddiff --git a/test/discovery.test.js b/test/discovery.test.js
iindex 1176b92..f094eb8 100644
---- a/test/discovery.test.js
++++ b/test/discovery.test.js
@@@ -85,7 +85,10 @@ function onHost(base, p, host) {
assert.match(r.headers.get('content-type'), /^text\/plain/);
assert.match(r.headers.get('cache-control') || '', /public, max-age=\d+/);
assert.ok(r.text.startsWith('# OpenVibe.Download'), 'no title');
-- assert.ok(r.text.includes('(https://openvibe.download/updates)'), 'no updates link');
++ // AGT-5: each of Download's own pages is linked at its Markdown twin, and the secondary links are Optional.
++ assert.ok(r.text.includes('(https://openvibe.download/index.md)'), 'the home link is not its twin');
++ assert.ok(r.text.includes('(https://openvibe.download/updates.md)'), 'no updates link');
++ assert.ok(r.text.includes('\n## Optional\n'), 'no ## Optional section');
assert.ok(r.text.includes('(https://openvibe.services)'), 'no pointer to the developer platform');
assert.ok(r.text.includes('https://openvibe.download/sitemap.xml'), 'sitemap not listed');
assert.ok(r.text.includes('(https://openvibe.download/llms-full.txt)'), 'llms-full.txt not listed');
ddiff --git a/test/pics-policy.test.js b/test/pics-policy.test.js
iindex 19e14fb..9346600 100644
---- a/test/pics-policy.test.js
++++ b/test/pics-policy.test.js
@@@ -102,7 +102,10 @@ const PAGES = ['/exif', '/content-policy', '/ai-training'];
const map = (await t.get('/sitemap.xml', { headers: PICS })).text;
for (const path of PAGES) assert.ok(map.includes(`<loc>https://openvibe.pics${path}</loc>`), `the sitemap lists ${path}`);
const llms = (await t.get('/llms.txt', { headers: PICS })).text;
-- for (const path of PAGES) assert.ok(llms.includes(`https://openvibe.pics${path}`), `llms.txt names ${path}`);
++ // AGT-5: the site's own pages are linked at their Markdown twins, and the secondary links end in Optional.
++ for (const path of PAGES) assert.ok(llms.includes(`https://openvibe.pics${path}.md`), `llms.txt names ${path}`);
++ assert.ok(llms.includes('](https://openvibe.pics/index.md)'), 'the home link is not its twin');
++ assert.ok(llms.includes('\n## Optional\n'), 'no ## Optional section');
// SEO-2: the Pics host's own agent manifest.
const agent = await t.get('/.well-known/agent.json', { headers: PICS });
assert.strictEqual(agent.status, 200);
ddiff --git a/test/video-pages.test.js b/test/video-pages.test.js
iindex 70f8404..ce533f4 100644
---- a/test/video-pages.test.js
++++ b/test/video-pages.test.js
@@@ -606,6 +606,9 @@ function countOccurrences(haystack, needle) {
const llms = await getVideo('/llms.txt');
assert.strictEqual(llms.status, 200);
assert.ok(llms.text.includes('# OpenVibe.Video'));
++ // AGT-5: Video's own pages are linked at their Markdown twins, and the secondary links end in Optional.
++ assert.match(llms.text, /\]\(https:\/\/openvibe\.video\/index\.md\)/, 'the home link is not its twin');
++ assert.ok(llms.text.includes('\n## Optional\n'), 'no ## Optional section');
// SEO-33: the same, in full.
const full = await getVideo('/llms-full.txt');
assert.strictEqual(full.status, 200);