openvibers/openvibe.mediahub Commits
MediaHub: openvibe.pics and openvibe.video are parked, not coming pages (plan rule: no placeholders) (#4)
Changes
@@@ -2,6 +2,11 @@What changed in OpenVibe.MediaHub, newest first. Each site also publishes its patch notes at /updates.++## Unreleased++++- openvibe.pics and openvibe.video are parked: their vhosts answer 302 to https://openvibe.network/ and proxy++ nothing. The app no longer has "coming" pages or host-based brand handling.++## 0.1.0 — 2026-10-08- openvibe.download v1, a private drive. Account-only uploads go into OpenVibe.Media: a plain form, or resumable@@@ -2,8 +2,9 @@> Your files, shared on your terms.--**Status:** alpha. One service behind three domains: **openvibe.download** (a private drive, live in v1),--**openvibe.pics** and **openvibe.video** (honest "coming" pages: no uploads until the safety tooling exists).++**Status:** alpha. **openvibe.download** (a private drive, live in v1) is the one domain this service serves.++**openvibe.pics** and **openvibe.video** are parked: they redirect to `https://openvibe.network/` until Pics and++Video are built, so no request for either host reaches the app.**Port:** 4990 · **Service id:** `media-hub` · **Env prefix:** `MEDIAHUB` · **License:** AGPL-3.0.## Why v1 is narrow@@@ -44,7 +45,7 @@ MediaHub owns the drive: `mh_files`, `mh_folders`, `mh_shares`, `mh_reports`, `m| Path | Who | What ||---|---|---|--| `/` | anyone | openvibe.download's home: what it is, the safety rules, sign in. On openvibe.pics and openvibe.video, their "coming" pages |++| `/` | anyone | openvibe.download's home: what it is, the safety rules, sign in. (openvibe.pics and openvibe.video are parked in nginx, not served here) || `/files`, `/files/:id` | the owner | your drive: folders, files, the usage bar, upload, share, delete || `/s/:slug` | a signed-in person | a share page: the name, the size, who shared it (their username), the expiry, Download, Report || `/staff` | staff | the reports queue: restore or remove |@@@ -92,8 +93,9 @@ npm test # every test/*.test.js on PGlite, with stand-ins for Network`sudo ovhost deploy media-hub` on the host (unit [deploy/systemd/openvibe-media-hub.service](deploy/systemd/openvibe-media-hub.service),env `/etc/openvibe/media-hub.env`). The vhost [deploy/nginx/openvibe.download.conf](deploy/nginx/openvibe.download.conf)--serves all three domains, each with its own certificate, and is installed with `ov-vhost-install`. Uploads stream--through nginx unbuffered; downloads are not buffered either.++serves openvibe.download and parks openvibe.pics and openvibe.video with a 302 to `https://openvibe.network/` (each++name with its own certificate); it is installed with `ov-vhost-install`. Uploads stream through nginx unbuffered;++downloads are not buffered either.## Security (threat notes)@@@ -2,16 +2,16 @@"stage": "alpha","code": true,"wave": "W20",-- "plan": "plan T10/T19: one media-hub service, three brands; v1 = openvibe.download as a private drive, pics and video wait for scanning and abuse intake",++ "plan": "plan T10/T19: media-hub serves openvibe.download as a private drive in v1; openvibe.pics and openvibe.video are parked (302 to openvibe.network) until scanning and abuse intake exist","runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9","port": "4990",-- "domain": "openvibe.download (openvibe.pics and openvibe.video answer with coming pages)",++ "domain": "openvibe.download (openvibe.pics and openvibe.video are parked: they redirect to openvibe.network)","packages": ["openvibe-contracts v0.122.1","openvibe-sdk v0.36.0","openvibe-shared v2.20.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.pics and openvibe.video are coming pages.",++ "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.pics and openvibe.video are parked and redirect to openvibe.network.","works": ["Sign-in with OpenVibe.Network (PKCE S256, httpOnly session cookies); pages server-rendered, complete without JavaScript","Uploads: a plain form streamed to disk with its sha256 (up to 256 MB), and resumable chunked uploads (8 MB parts) through Media multipart",@@@ -19,12 +19,12 @@"Share links: 1 hour to 7 days, revocable, counted, sign-in required, optional named accounts (Network subjects only)","Reports: 3 distinct people or one report of illegal content suspend a share; staff restore or remove (Media object deleted)","Downloads: attachment, nosniff, sandbox CSP, real type only for a safe list, filename* for any script, Range resume",-- "Host-header brands: openvibe.pics and openvibe.video coming pages, no upload there",++ "Parked domains: openvibe.pics and openvibe.video redirect to openvibe.network in nginx; no request reaches the app","Account export and deletion (ADR-033) through openvibe-sdk/account-data; subscriptions created at boot","Per-caller and per-address limits; readiness (database required); metrics on loopback"],"notYet": [-- "Uploads on openvibe.pics (images, albums, embeds) and openvibe.video (watch pages, captions, playlists): they wait for scanning (hash lists, malware) and abuse intake shared with Media holds",++ "Uploads on openvibe.pics (images, albums, embeds) and openvibe.video (watch pages, captions, playlists): both domains are parked until scanning (hash lists, malware) and abuse intake shared with Media holds exist","Public (signed-out) share links: deliberately not offered while there is no scanning","Previews and thumbnails of files"],@@@ -153,98 +153,57 @@ server {}}--# ── openvibe.pics and openvibe.video: the same process answers by Host with "coming" pages (no uploads there) ──++# ── openvibe.pics and openvibe.video: parked. They redirect to openvibe.network until Pics and Video are built. ──++# No app is behind these hosts: every request is answered here with a 302, and nothing is proxied.server {listen 80;listen [::]:80;server_name openvibe.pics www.openvibe.pics openvibe.video www.openvibe.video;location /.well-known/acme-challenge/ { root /var/www/certbot; }-- location / { return 301 https://$host$request_uri; }++ location / { return 302 https://openvibe.network/; }}server {listen 443 ssl;listen [::]:443 ssl;http2 on;-- server_name www.openvibe.pics;++ server_name openvibe.pics;ssl_certificate /etc/letsencrypt/live/openvibe.pics/fullchain.pem;ssl_certificate_key /etc/letsencrypt/live/openvibe.pics/privkey.pem;-- return 301 https://openvibe.pics$request_uri;++ ssl_protocols TLSv1.2 TLSv1.3;++ return 302 https://openvibe.network/;}server {listen 443 ssl;listen [::]:443 ssl;http2 on;-- server_name www.openvibe.video;-- ssl_certificate /etc/letsencrypt/live/openvibe.video/fullchain.pem;-- ssl_certificate_key /etc/letsencrypt/live/openvibe.video/privkey.pem;-- return 301 https://openvibe.video$request_uri;++ server_name www.openvibe.pics;++ ssl_certificate /etc/letsencrypt/live/openvibe.pics/fullchain.pem;++ ssl_certificate_key /etc/letsencrypt/live/openvibe.pics/privkey.pem;++ ssl_protocols TLSv1.2 TLSv1.3;++ return 302 https://openvibe.network/;}server {listen 443 ssl;listen [::]:443 ssl;http2 on;-- server_name openvibe.pics;-- include snippets/security-txt.conf;-- ssl_certificate /etc/letsencrypt/live/openvibe.pics/fullchain.pem;-- ssl_certificate_key /etc/letsencrypt/live/openvibe.pics/privkey.pem;++ server_name openvibe.video;++ ssl_certificate /etc/letsencrypt/live/openvibe.video/fullchain.pem;++ ssl_certificate_key /etc/letsencrypt/live/openvibe.video/privkey.pem;ssl_protocols TLSv1.2 TLSv1.3;-- access_log /var/log/nginx/openvibe.pics.access.log;-- error_log /var/log/nginx/openvibe.pics.error.log;-- client_max_body_size 128k;-- add_header X-Content-Type-Options nosniff always;-- add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;-- gzip on;-- gzip_types text/css application/javascript application/json text/plain application/xml;-- 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;-- location /auth/ {-- limit_req zone=media-hub_auth burst=10 nodelay;-- limit_req_status 429;-- proxy_pass http://127.0.0.1:4990;-- }-- location /internal/ { return 404; }-- location = /metrics { return 404; }-- location / { proxy_pass http://127.0.0.1:4990; }-- location ~ /\.(?!well-known) { deny all; }++ return 302 https://openvibe.network/;}server {listen 443 ssl;listen [::]:443 ssl;http2 on;-- server_name openvibe.video;-- include snippets/security-txt.conf;++ server_name www.openvibe.video;ssl_certificate /etc/letsencrypt/live/openvibe.video/fullchain.pem;ssl_certificate_key /etc/letsencrypt/live/openvibe.video/privkey.pem;ssl_protocols TLSv1.2 TLSv1.3;-- access_log /var/log/nginx/openvibe.video.access.log;-- error_log /var/log/nginx/openvibe.video.error.log;-- client_max_body_size 128k;-- add_header X-Content-Type-Options nosniff always;-- add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;-- gzip on;-- gzip_types text/css application/javascript application/json text/plain application/xml;-- 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;-- location /auth/ {-- limit_req zone=media-hub_auth burst=10 nodelay;-- limit_req_status 429;-- proxy_pass http://127.0.0.1:4990;-- }-- location /internal/ { return 404; }-- location = /metrics { return 404; }-- location / { proxy_pass http://127.0.0.1:4990; }-- location ~ /\.(?!well-known) { deny all; }++ return 302 https://openvibe.network/;}@@@ -32,7 +32,6 @@ const { createServiceReadiness } = require('./observability');const { createCallerLimits } = require('./http/caller-limits');const { assetVersion, send } = require('./render/layout');const { html } = require('./render/html');--const brand = require('./brand');const store = require('./drive/store');const { createMediaClient } = require('./media/client');const { createIdentity } = require('./network/identity');@@@ -101,9 +100,6 @@ async function createApp(opts = {}) {}));app.use(cookieParser());-- // ── Which brand this request is for (the Host header decides; server/brand.js) ──-- app.use(brand.middleware());--// ── Machine endpoints ───────────────────────────────────app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-media-hub', version: VERSION }));release.mount(app, { registry: metrics.registry });ddeleted file mode 100644@@@ -1,51 +0,0 @@--'use strict';----/**-- * One process, three brand surfaces (the media-hub product manifest: openvibe.download, openvibe.pics and-- * openvibe.video). The host decides which one a request is for — there is no separate deploy, and no cookie or-- * query parameter can move a request from one brand to another.-- *-- * v1 launches openvibe.download only, as a private drive. On openvibe.pics and openvibe.video the service answers-- * an honest "coming" page and nothing else: no drive, no share links, no API and no uploads, because neither brand-- * has the safety tooling its content needs yet (there is no malware or CSAM scanning on the network).-- *-- * The brand is read from the Host header nginx passes through (req.hostname; TRUST_PROXY lets a proxy name it with-- * X-Forwarded-Host). An unknown host — a bare address in development, an internal health check — is the download-- * brand, so the deployed apex never behaves differently from a test.-- */--const BRANDS = {-- 'openvibe.download': 'download',-- 'openvibe.pics': 'pics',-- 'openvibe.video': 'video',--};----// What each unlaunched brand's page says: the product's own words, kept here so the page and the report agree.--const COMING = {-- pics: {-- domain: 'openvibe.pics',-- name: 'OpenVibe.Pics',-- tagline: 'Share pictures. Fast, open, yours.',-- what: 'OpenVibe.Pics will be the picture home of the OpenVibe network: quick uploads and share links, albums and galleries, image processing and optimisation served from a shared CDN, and AI image tools — an open and modular alternative to Imgur, usable through the OpenVibe API too.',-- pillars: ['Upload & share', 'Albums & galleries', 'Processing', 'AI image tools'],-- },-- video: {-- domain: 'openvibe.video',-- name: 'OpenVibe.Video',-- tagline: 'Your videos, everywhere.',-- what: 'OpenVibe.Video will be the home of long-form video on the OpenVibe network: watch pages, channels and playlists for uploads and past streams, a creator tool that publishes one upload to many platforms, and AI captions, highlights and editing — delivered by OpenVibe.Media.',-- pillars: ['Watch', 'Publish everywhere', 'AI tools', 'From your streams'],-- },--};----/** The brand a request is for. Anything unrecognised is the launched one. */--function brandOf(req) {-- const host = String((req && req.hostname) || '').toLowerCase().replace(/^www\./, '');-- return BRANDS[host] || 'download';--}----/** Set req.brand for every request, before anything reads it (the API and the pages both do). */--function middleware() {-- return (req, _res, next) => { req.brand = brandOf(req); next(); };--}----module.exports = { brandOf, middleware, BRANDS, COMING };@@@ -57,10 +57,6 @@ function createApi(ctx) {const problem = (req, res, status, code, detail) => contracts.http.sendProblem(res, status, code, { detail, ctx: req.ov });r.use((req, res, next) => { res.set('Cache-Control', 'no-store'); next(); });-- // openvibe.pics and openvibe.video have no API: no upload, no drive, no share links (server/brand.js).-- r.use((req, res, next) => (req.brand && req.brand !== 'download'-- ? contracts.http.sendProblem(res, 404, 'route.not_found', { detail: 'This brand of the hub has no API yet; OpenVibe.Download is the one that has launched.', ctx: req.ov })-- : next()));r.use(principal.middleware);/**@@@ -42,7 +42,7 @@ function homeJsonLd(config) {}/**-- * The pages a crawler may read: this brand's homes, the safety rules and the update log — nothing behind sign-in.++ * The pages a crawler may read: the home page, the safety rules and the update log — nothing behind sign-in.* /files, /s/… and /staff are a person's own, so they are disallowed above and left out here.*/const publicPages = () => [@@@ -1,8 +1,7 @@'use strict';/**-- * The pages of openvibe.download, plus the honest "coming" homes of openvibe.pics and openvibe.video (the host-- * decides which, see server/brand.js).++ * The pages of openvibe.download.** / what this is, the safety rules and how to sign in* /files your drive: folders, files, the usage bar, the upload form@@@ -42,7 +41,6 @@ const { send } = require('../render/layout');const rules = require('../drive/rules');const { isStaff } = require('../drive/staff');const { boundaryOf, readFormData } = require('./form-upload');--const { brandOf, COMING } = require('../brand');const SITE_NAME = 'OpenVibe.MediaHub';const TAGLINE = 'Your files, shared on your terms.';@@@ -142,66 +140,7 @@ ${notice(p && p.detail ? p.detail : 'You are over the limit for this.', 'warn')}body: html`<h1>Sign in first</h1>${signInPrompt(why, back === 'here' ? req.originalUrl : back)}`,}, 401));-- // ── The unlaunched brands ────────────────────────────────-- /**-- * openvibe.pics and openvibe.video answer an honest "coming" page and nothing else: no drive, no share links,-- * no uploads. /updates and the crawl artifacts are the same on every brand, and everything else 404s — the-- * product has not shipped there, so there is nothing to offer but the truth.-- */-- const PASS_THROUGH = new Set(['/robots.txt', '/sitemap.xml', '/llms.txt', '/llms-full.txt', '/release.json', '/auth/me', '/favicon.ico']);-- r.use((req, res, next) => {-- if (brandOf(req) === 'download') return next();-- if (PASS_THROUGH.has(req.path)) return next();-- if (req.path === '/') return next(); // the brand's own home, below-- if (req.path === '/updates') return next();-- const brand = COMING[brandOf(req)];-- return page(req, res, {-- title: `${brand.name} is coming`,-- crumbs: [{ label: 'Home', href: '/' }, { label: 'Coming' }],-- body: html`<h1>${brand.name} is not open yet</h1>--${notice(`${brand.name} has no uploads and no file pages yet. The only brand of this service that has launched is OpenVibe.Download.`, 'warn')}--<p class="muted">This host serves ${brand.domain}, one of the three brands of the same OpenVibe.MediaHub service. <a href="https://openvibe.download">Go to OpenVibe.Download</a> for the part that works today.</p>`,-- }, 404);-- });--// ── Home ─────────────────────────────────────────────────-- const comingBody = (brand) => html`${raw(showcase.hero({-- eyebrow: `${brand.name} · ${brand.tagline}`,-- title: brand.name,-- accent: 'coming soon',-- lede: brand.what,-- actions: [{ label: 'OpenVibe.Download is open', href: 'https://openvibe.download', primary: true }, { label: 'What shipped', href: '/updates' }],-- note: 'Nothing is uploaded here yet and there is no way to sign in on this brand.',-- }))}--${raw(showcase.features({-- title: 'What it will be',-- lede: 'The shape of it, from the product\'s own plan.',-- items: brand.pillars.map((p) => ({ icon: 'ov:page', title: p, text: '' })),-- }))}--${raw(showcase.features({-- title: 'Why it is not open yet',-- lede: 'Two things have to be true before OpenVibe runs a public place for other people\'s pictures and videos.',-- items: [-- { icon: 'ov:shield', title: 'Safety tooling first', text: 'There is no malware or CSAM scanning on the network yet, so uploads here wait for it. OpenVibe.Download opens first precisely because it is a private drive whose share links only signed-in people can open.' },-- { icon: 'ov:page', title: 'One service, three brands', text: `${brand.name} is served by the same process as openvibe.download — the same file store, the same account and the same rules. This host answers only this page until that tooling is here.` },-- ],-- }))}--${raw(showcase.cta({ title: 'In the meantime', text: 'OpenVibe.Media already stores and serves the network\'s pictures and video, and OpenVibe.Download is open for private files.', actions: [{ label: 'OpenVibe.Media', href: 'https://openvibe.media' }, { label: 'OpenVibe.Download', href: 'https://openvibe.download' }] }))}`;---- r.get('/', (req, res, next) => {-- const brand = COMING[brandOf(req)];-- if (brand) {-- return page(req, res, {-- index: true, cache: PUBLIC_CACHE, styles: [showcase.STYLESHEET],-- title: `${brand.name} — coming soon`,-- description: `${brand.name}: ${brand.tagline} Not open yet — uploads here wait for safety tooling. OpenVibe.Download is the brand of this service that has launched.`,-- crumbs: [{ label: 'Home', href: '/' }],-- body: comingBody(brand),-- });-- }-- return next();-- });--r.get('/', (req, res) => {const hero = showcase.hero({eyebrow: `${SITE_NAME} · ${TAGLINE}`,ddeleted file mode 100644@@@ -1,80 +0,0 @@--'use strict';--/**-- * One process, three brand surfaces (the media-hub product manifest). The Host header decides which one a request-- * is for, and v1 has launched only openvibe.download: openvibe.pics and openvibe.video answer an honest "coming"-- * page and nothing else — no drive, no share links, no API and above all no uploads, because neither brand has the-- * safety tooling its content needs yet.-- */--const assert = require('assert');--const { boot, check, done } = require('./helpers/boot');--const { SAME, bytes, upload, share } = require('./helpers/drive');----const PICS = { 'x-forwarded-host': 'openvibe.pics' };--const VIDEO = { 'x-forwarded-host': 'openvibe.video' };----(async () => {-- const t = await boot();-- const kim = t.network.addUser('kim');-- const other = t.network.addUser('sam');-- const file = (await upload(t, kim, bytes(120, 'brand'), { name: 'brand.bin' })).json().file;-- const slug = (await share(t, kim, { file_id: file.id, hours: 24 })).json().share.slug;---- await check('openvibe.pics and openvibe.video answer a coming home that says what they will be', async () => {-- const pics = await t.get('/', { headers: PICS });-- assert.strictEqual(pics.status, 200);-- assert.match(pics.headers.get('content-type'), /^text\/html/);-- assert.ok(pics.text.includes('OpenVibe.Pics'), 'the brand');-- assert.ok(pics.text.includes('Share pictures. Fast, open, yours.'), 'its own tagline');-- assert.ok(pics.text.includes('Albums & galleries') || pics.text.includes('Albums & galleries'), 'what it will be');-- assert.ok(/safety tooling/i.test(pics.text), 'why it is not open yet');-- assert.ok(/no malware or CSAM scanning/i.test(pics.text), 'said plainly');-- assert.ok(pics.text.includes('https://openvibe.download'), 'a link to the brand that has launched');---- const video = await t.get('/', { headers: VIDEO });-- assert.strictEqual(video.status, 200);-- assert.ok(video.text.includes('OpenVibe.Video'));-- assert.ok(video.text.includes('Your videos, everywhere.'));-- assert.ok(/safety tooling/i.test(video.text));-- assert.ok(video.text.includes('https://openvibe.download'));-- });---- await check('nothing else exists on those brands: no drive, no share, no API, no upload', async () => {-- const before = t.media.requests.length;-- for (const headers of [PICS, VIDEO]) {-- const brand = headers['x-forwarded-host'];-- for (const p of ['/files', `/files/${file.id}`, `/files/${file.id}/download`, `/s/${slug}`, `/s/${slug}/download`, `/staff`, '/api/v1/ping']) {-- const r = await t.get(p, { as: kim, headers });-- assert.strictEqual(r.status, 404, `${brand} ${p} answered ${r.status}`);-- }-- // The write routes too — with a real file behind them.-- const up = await t.get('/api/v1/files', { as: kim, headers: { ...headers, ...SAME }, json: { name: 'x.bin', size: 10, content_type: 'text/plain' } });-- assert.strictEqual(up.status, 404, `${brand}: the API is not there`);-- const form = await t.get('/files/upload', { as: kim, method: 'POST', headers: { ...headers, ...SAME, 'content-type': 'application/x-www-form-urlencoded' }, body: 'name=x' });-- assert.strictEqual(form.status, 404, `${brand}: no form upload`);-- }-- assert.strictEqual(t.media.requests.length, before, 'and OpenVibe.Media was never called');-- });---- await check('the crawl artifacts and the update log are the same on every brand', async () => {-- for (const headers of [{}, PICS, VIDEO]) {-- assert.strictEqual((await t.get('/updates', { headers })).status, 200);-- assert.strictEqual((await t.get('/robots.txt', { headers })).status, 200);-- assert.strictEqual((await t.get('/sitemap.xml', { headers })).status, 200);-- assert.strictEqual((await t.get('/llms.txt', { headers })).status, 200);-- }-- });---- await check('the launched brand is untouched, and an unknown host is the launched brand', async () => {-- assert.ok((await t.get('/')).text.includes('OpenVibe.Download'));-- assert.ok((await t.get('/')).text.includes('private drive'));-- for (const headers of [{ 'x-forwarded-host': 'openvibe.download' }, { 'x-forwarded-host': 'www.openvibe.download' }, { 'x-forwarded-host': '127.0.0.1' }, { 'x-forwarded-host': 'openvibe.host' }]) {-- const r = await t.get('/', { headers });-- assert.ok(r.text.includes('OpenVibe.Download'), JSON.stringify(headers));-- }-- assert.strictEqual((await t.get('/files', { as: kim })).status, 200);-- assert.strictEqual((await t.get(`/s/${slug}`, { as: other })).status, 200, 'and its share links work');-- });---- await t.close();-- done();--})().catch((e) => { console.error(e); process.exit(1); });nnew file mode 100644@@@ -0,0 +1,29 @@++'use strict';++// deploy/nginx/openvibe.download.conf: openvibe.pics and openvibe.video are parked. Their server blocks (HTTP and++// HTTPS, apex and www) answer 302 to openvibe.network and proxy nothing — no request for either host reaches the++// app, which is why the app has no "coming" pages for them. Each name keeps its own certificate.++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++const conf = fs.readFileSync(path.join(__dirname, '..', ...'deploy/nginx/openvibe.download.conf'.split('/')), 'utf8');++const blocks = [...conf.matchAll(/(?:^|\n)server \{([\s\S]*?)\n\}/g)].map((m) => m[1]);++const named = (host) => blocks.filter((b) => new RegExp(`server_name [^;]*\\b${host.replace(/\./g, '\\.')}\\b`).test(b));++++for (const host of ['openvibe.pics', 'openvibe.video']) {++ const parked = named(host);++ assert.ok(parked.length >= 3, `${host}: the HTTP block and the apex and www HTTPS blocks are there`);++ for (const b of parked) {++ assert.match(b, /return 302 https:\/\/openvibe\.network\/;/, `${host}: redirects to openvibe.network`);++ assert.doesNotMatch(b, /proxy_pass/, `${host}: proxies nothing`);++ }++}++++// Each host uses its own certificate, on both its apex and its www block.++for (const host of ['openvibe.pics', 'openvibe.video']) {++ for (const b of named(host).filter((x) => /listen 443 ssl;/.test(x))) {++ assert.match(b, new RegExp(`ssl_certificate\\s+/etc/letsencrypt/live/${host.replace(/\./g, '\\.')}/fullchain\\.pem;`), `${host}: its own certificate`);++ }++}++++console.log('nginx parked domains: pics and video answer 302 openvibe.network, no proxy_pass, own certificates');
@@ -2,6 +2,11 @@
@@ -2,6 +2,11 @@
What changed in OpenVibe.MediaHub, newest first. Each site also publishes its patch notes at /updates.
What changed in OpenVibe.MediaHub, newest first. Each site also publishes its patch notes at /updates.
+## Unreleased
+
+- openvibe.pics and openvibe.video are parked: their vhosts answer 302 to https://openvibe.network/ and proxy
+ nothing. The app no longer has "coming" pages or host-based brand handling.
+
## 0.1.0 — 2026-10-08
## 0.1.0 — 2026-10-08
- openvibe.download v1, a private drive. Account-only uploads go into OpenVibe.Media: a plain form, or resumable
- openvibe.download v1, a private drive. Account-only uploads go into OpenVibe.Media: a plain form, or resumable
@@ -2,8 +2,9 @@
@@ -2,8 +2,9 @@
> Your files, shared on your terms.
> Your files, shared on your terms.
-**Status:** alpha. One service behind three domains: **openvibe.download** (a private drive, live in v1),
-**openvibe.pics** and **openvibe.video** (honest "coming" pages: no uploads until the safety tooling exists).
+**Status:** alpha. **openvibe.download** (a private drive, live in v1) is the one domain this service serves.
+**openvibe.pics** and **openvibe.video** are parked: they redirect to `https://openvibe.network/` until Pics and
+Video are built, so no request for either host reaches the app.
**Port:** 4990 · **Service id:** `media-hub` · **Env prefix:** `MEDIAHUB` · **License:** AGPL-3.0.
**Port:** 4990 · **Service id:** `media-hub` · **Env prefix:** `MEDIAHUB` · **License:** AGPL-3.0.
## Why v1 is narrow
## Why v1 is narrow
@@ -44,7 +45,7 @@ MediaHub owns the drive: `mh_files`, `mh_folders`, `mh_shares`, `mh_reports`, `m
@@ -44,7 +45,7 @@ MediaHub owns the drive: `mh_files`, `mh_folders`, `mh_shares`, `mh_reports`, `m
| Path | Who | What |
| Path | Who | What |
|---|---|---|
|---|---|---|
-| `/` | anyone | openvibe.download's home: what it is, the safety rules, sign in. On openvibe.pics and openvibe.video, their "coming" pages |
+| `/` | anyone | openvibe.download's home: what it is, the safety rules, sign in. (openvibe.pics and openvibe.video are parked in nginx, not served here) |
| `/files`, `/files/:id` | the owner | your drive: folders, files, the usage bar, upload, share, delete |
| `/files`, `/files/:id` | the owner | your drive: folders, files, the usage bar, upload, share, delete |
| `/s/:slug` | a signed-in person | a share page: the name, the size, who shared it (their username), the expiry, Download, Report |
| `/s/:slug` | a signed-in person | a share page: the name, the size, who shared it (their username), the expiry, Download, Report |
| `/staff` | staff | the reports queue: restore or remove |
| `/staff` | staff | the reports queue: restore or remove |
@@ -92,8 +93,9 @@ npm test # every test/*.test.js on PGlite, with stand-ins for Network
@@ -92,8 +93,9 @@ npm test # every test/*.test.js on PGlite, with stand-ins for Network
`sudo ovhost deploy media-hub` on the host (unit [deploy/systemd/openvibe-media-hub.service](deploy/systemd/openvibe-media-hub.service),
`sudo ovhost deploy media-hub` on the host (unit [deploy/systemd/openvibe-media-hub.service](deploy/systemd/openvibe-media-hub.service),
env `/etc/openvibe/media-hub.env`). The vhost [deploy/nginx/openvibe.download.conf](deploy/nginx/openvibe.download.conf)
env `/etc/openvibe/media-hub.env`). The vhost [deploy/nginx/openvibe.download.conf](deploy/nginx/openvibe.download.conf)
-serves all three domains, each with its own certificate, and is installed with `ov-vhost-install`. Uploads stream
-through nginx unbuffered; downloads are not buffered either.
+serves openvibe.download and parks openvibe.pics and openvibe.video with a 302 to `https://openvibe.network/` (each
+name with its own certificate); it is installed with `ov-vhost-install`. Uploads stream through nginx unbuffered;
+downloads are not buffered either.
## Security (threat notes)
## Security (threat notes)
@@ -2,16 +2,16 @@
@@ -2,16 +2,16 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W20",
"wave": "W20",
- "plan": "plan T10/T19: one media-hub service, three brands; v1 = openvibe.download as a private drive, pics and video wait for scanning and abuse intake",
+ "plan": "plan T10/T19: media-hub serves openvibe.download as a private drive in v1; openvibe.pics and openvibe.video are parked (302 to openvibe.network) until scanning and abuse intake exist",
"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": "4990",
"port": "4990",
- "domain": "openvibe.download (openvibe.pics and openvibe.video answer with coming pages)",
+ "domain": "openvibe.download (openvibe.pics and openvibe.video are parked: they redirect to openvibe.network)",
"packages": [
"packages": [
"openvibe-contracts v0.122.1",
"openvibe-contracts v0.122.1",
"openvibe-sdk v0.36.0",
"openvibe-sdk v0.36.0",
"openvibe-shared v2.20.0"
"openvibe-shared v2.20.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.pics and openvibe.video are coming pages.",
+ "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.pics and openvibe.video are parked and redirect to openvibe.network.",
"works": [
"works": [
"Sign-in with OpenVibe.Network (PKCE S256, httpOnly session cookies); pages server-rendered, complete without JavaScript",
"Sign-in with OpenVibe.Network (PKCE S256, httpOnly session cookies); pages server-rendered, complete without JavaScript",
"Uploads: a plain form streamed to disk with its sha256 (up to 256 MB), and resumable chunked uploads (8 MB parts) through Media multipart",
"Uploads: a plain form streamed to disk with its sha256 (up to 256 MB), and resumable chunked uploads (8 MB parts) through Media multipart",
@@ -19,12 +19,12 @@
@@ -19,12 +19,12 @@
"Share links: 1 hour to 7 days, revocable, counted, sign-in required, optional named accounts (Network subjects only)",
"Share links: 1 hour to 7 days, revocable, counted, sign-in required, optional named accounts (Network subjects only)",
"Reports: 3 distinct people or one report of illegal content suspend a share; staff restore or remove (Media object deleted)",
"Reports: 3 distinct people or one report of illegal content suspend a share; staff restore or remove (Media object deleted)",
"Downloads: attachment, nosniff, sandbox CSP, real type only for a safe list, filename* for any script, Range resume",
"Downloads: attachment, nosniff, sandbox CSP, real type only for a safe list, filename* for any script, Range resume",
- "Host-header brands: openvibe.pics and openvibe.video coming pages, no upload there",
+ "Parked domains: openvibe.pics and openvibe.video redirect to openvibe.network in nginx; no request reaches the app",
"Account export and deletion (ADR-033) through openvibe-sdk/account-data; subscriptions created at boot",
"Account export and deletion (ADR-033) through openvibe-sdk/account-data; subscriptions created at boot",
"Per-caller and per-address limits; readiness (database required); metrics on loopback"
"Per-caller and per-address limits; readiness (database required); metrics on loopback"
],
],
"notYet": [
"notYet": [
- "Uploads on openvibe.pics (images, albums, embeds) and openvibe.video (watch pages, captions, playlists): they wait for scanning (hash lists, malware) and abuse intake shared with Media holds",
+ "Uploads on openvibe.pics (images, albums, embeds) and openvibe.video (watch pages, captions, playlists): both domains are parked until scanning (hash lists, malware) and abuse intake shared with Media holds exist",
"Public (signed-out) share links: deliberately not offered while there is no scanning",
"Public (signed-out) share links: deliberately not offered while there is no scanning",
"Previews and thumbnails of files"
"Previews and thumbnails of files"
],
],
@@ -153,98 +153,57 @@ server {
@@ -153,98 +153,57 @@ server {
}
}
}
}
-# ── openvibe.pics and openvibe.video: the same process answers by Host with "coming" pages (no uploads there) ──
+# ── openvibe.pics and openvibe.video: parked. They redirect to openvibe.network until Pics and Video are built. ──
+# No app is behind these hosts: every request is answered here with a 302, and nothing is proxied.
server {
server {
listen 80;
listen 80;
listen [::]:80;
listen [::]:80;
server_name openvibe.pics www.openvibe.pics openvibe.video www.openvibe.video;
server_name openvibe.pics www.openvibe.pics openvibe.video www.openvibe.video;
location /.well-known/acme-challenge/ { root /var/www/certbot; }
location /.well-known/acme-challenge/ { root /var/www/certbot; }
- location / { return 301 https://$host$request_uri; }
+ location / { return 302 https://openvibe.network/; }
}
}
server {
server {
listen 443 ssl;
listen 443 ssl;
listen [::]:443 ssl;
listen [::]:443 ssl;
http2 on;
http2 on;
- server_name www.openvibe.pics;
+ server_name openvibe.pics;
ssl_certificate /etc/letsencrypt/live/openvibe.pics/fullchain.pem;
ssl_certificate /etc/letsencrypt/live/openvibe.pics/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/openvibe.pics/privkey.pem;
ssl_certificate_key /etc/letsencrypt/live/openvibe.pics/privkey.pem;
- return 301 https://openvibe.pics$request_uri;
+ ssl_protocols TLSv1.2 TLSv1.3;
+ return 302 https://openvibe.network/;
}
}
server {
server {
listen 443 ssl;
listen 443 ssl;
listen [::]:443 ssl;
listen [::]:443 ssl;
http2 on;
http2 on;
- server_name www.openvibe.video;
- ssl_certificate /etc/letsencrypt/live/openvibe.video/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/openvibe.video/privkey.pem;
- return 301 https://openvibe.video$request_uri;
+ server_name www.openvibe.pics;
+ ssl_certificate /etc/letsencrypt/live/openvibe.pics/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/openvibe.pics/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+ return 302 https://openvibe.network/;
}
}
server {
server {
listen 443 ssl;
listen 443 ssl;
listen [::]:443 ssl;
listen [::]:443 ssl;
http2 on;
http2 on;
- server_name openvibe.pics;
- include snippets/security-txt.conf;
- ssl_certificate /etc/letsencrypt/live/openvibe.pics/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/openvibe.pics/privkey.pem;
+ server_name openvibe.video;
+ ssl_certificate /etc/letsencrypt/live/openvibe.video/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/openvibe.video/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_protocols TLSv1.2 TLSv1.3;
- access_log /var/log/nginx/openvibe.pics.access.log;
- error_log /var/log/nginx/openvibe.pics.error.log;
- client_max_body_size 128k;
- add_header X-Content-Type-Options nosniff always;
- add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
- gzip on;
- gzip_types text/css application/javascript application/json text/plain application/xml;
- 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;
- location /auth/ {
- limit_req zone=media-hub_auth burst=10 nodelay;
- limit_req_status 429;
- proxy_pass http://127.0.0.1:4990;
- }
- location /internal/ { return 404; }
- location = /metrics { return 404; }
- location / { proxy_pass http://127.0.0.1:4990; }
- location ~ /\.(?!well-known) { deny all; }
+ return 302 https://openvibe.network/;
}
}
server {
server {
listen 443 ssl;
listen 443 ssl;
listen [::]:443 ssl;
listen [::]:443 ssl;
http2 on;
http2 on;
- server_name openvibe.video;
- include snippets/security-txt.conf;
+ server_name www.openvibe.video;
ssl_certificate /etc/letsencrypt/live/openvibe.video/fullchain.pem;
ssl_certificate /etc/letsencrypt/live/openvibe.video/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/openvibe.video/privkey.pem;
ssl_certificate_key /etc/letsencrypt/live/openvibe.video/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_protocols TLSv1.2 TLSv1.3;
- access_log /var/log/nginx/openvibe.video.access.log;
- error_log /var/log/nginx/openvibe.video.error.log;
- client_max_body_size 128k;
- add_header X-Content-Type-Options nosniff always;
- add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
- gzip on;
- gzip_types text/css application/javascript application/json text/plain application/xml;
- 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;
- location /auth/ {
- limit_req zone=media-hub_auth burst=10 nodelay;
- limit_req_status 429;
- proxy_pass http://127.0.0.1:4990;
- }
- location /internal/ { return 404; }
- location = /metrics { return 404; }
- location / { proxy_pass http://127.0.0.1:4990; }
- location ~ /\.(?!well-known) { deny all; }
+ return 302 https://openvibe.network/;
}
}
@@ -32,7 +32,6 @@ const { createServiceReadiness } = require('./observability');
@@ -32,7 +32,6 @@ const { createServiceReadiness } = require('./observability');
const { createCallerLimits } = require('./http/caller-limits');
const { createCallerLimits } = require('./http/caller-limits');
const { assetVersion, send } = require('./render/layout');
const { assetVersion, send } = require('./render/layout');
const { html } = require('./render/html');
const { html } = require('./render/html');
-const brand = require('./brand');
const store = require('./drive/store');
const store = require('./drive/store');
const { createMediaClient } = require('./media/client');
const { createMediaClient } = require('./media/client');
const { createIdentity } = require('./network/identity');
const { createIdentity } = require('./network/identity');
@@ -101,9 +100,6 @@ async function createApp(opts = {}) {
@@ -101,9 +100,6 @@ async function createApp(opts = {}) {
}));
}));
app.use(cookieParser());
app.use(cookieParser());
- // ── Which brand this request is for (the Host header decides; server/brand.js) ──
- app.use(brand.middleware());
-
// ── Machine endpoints ───────────────────────────────────
// ── Machine endpoints ───────────────────────────────────
app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-media-hub', version: VERSION }));
app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-media-hub', version: VERSION }));
release.mount(app, { registry: metrics.registry });
release.mount(app, { registry: metrics.registry });
deleted file mode 100644
deleted file mode 100644
@@ -1,51 +0,0 @@
@@ -1,51 +0,0 @@
-'use strict';
-
-/**
- * One process, three brand surfaces (the media-hub product manifest: openvibe.download, openvibe.pics and
- * openvibe.video). The host decides which one a request is for — there is no separate deploy, and no cookie or
- * query parameter can move a request from one brand to another.
- *
- * v1 launches openvibe.download only, as a private drive. On openvibe.pics and openvibe.video the service answers
- * an honest "coming" page and nothing else: no drive, no share links, no API and no uploads, because neither brand
- * has the safety tooling its content needs yet (there is no malware or CSAM scanning on the network).
- *
- * The brand is read from the Host header nginx passes through (req.hostname; TRUST_PROXY lets a proxy name it with
- * X-Forwarded-Host). An unknown host — a bare address in development, an internal health check — is the download
- * brand, so the deployed apex never behaves differently from a test.
- */
-const BRANDS = {
- 'openvibe.download': 'download',
- 'openvibe.pics': 'pics',
- 'openvibe.video': 'video',
-};
-
-// What each unlaunched brand's page says: the product's own words, kept here so the page and the report agree.
-const COMING = {
- pics: {
- domain: 'openvibe.pics',
- name: 'OpenVibe.Pics',
- tagline: 'Share pictures. Fast, open, yours.',
- what: 'OpenVibe.Pics will be the picture home of the OpenVibe network: quick uploads and share links, albums and galleries, image processing and optimisation served from a shared CDN, and AI image tools — an open and modular alternative to Imgur, usable through the OpenVibe API too.',
- pillars: ['Upload & share', 'Albums & galleries', 'Processing', 'AI image tools'],
- },
- video: {
- domain: 'openvibe.video',
- name: 'OpenVibe.Video',
- tagline: 'Your videos, everywhere.',
- what: 'OpenVibe.Video will be the home of long-form video on the OpenVibe network: watch pages, channels and playlists for uploads and past streams, a creator tool that publishes one upload to many platforms, and AI captions, highlights and editing — delivered by OpenVibe.Media.',
- pillars: ['Watch', 'Publish everywhere', 'AI tools', 'From your streams'],
- },
-};
-
-/** The brand a request is for. Anything unrecognised is the launched one. */
-function brandOf(req) {
- const host = String((req && req.hostname) || '').toLowerCase().replace(/^www\./, '');
- return BRANDS[host] || 'download';
-}
-
-/** Set req.brand for every request, before anything reads it (the API and the pages both do). */
-function middleware() {
- return (req, _res, next) => { req.brand = brandOf(req); next(); };
-}
-
-module.exports = { brandOf, middleware, BRANDS, COMING };
@@ -57,10 +57,6 @@ function createApi(ctx) {
@@ -57,10 +57,6 @@ function createApi(ctx) {
const problem = (req, res, status, code, detail) => contracts.http.sendProblem(res, status, code, { detail, ctx: req.ov });
const problem = (req, res, status, code, detail) => contracts.http.sendProblem(res, status, code, { detail, ctx: req.ov });
r.use((req, res, next) => { res.set('Cache-Control', 'no-store'); next(); });
r.use((req, res, next) => { res.set('Cache-Control', 'no-store'); next(); });
- // openvibe.pics and openvibe.video have no API: no upload, no drive, no share links (server/brand.js).
- r.use((req, res, next) => (req.brand && req.brand !== 'download'
- ? contracts.http.sendProblem(res, 404, 'route.not_found', { detail: 'This brand of the hub has no API yet; OpenVibe.Download is the one that has launched.', ctx: req.ov })
- : next()));
r.use(principal.middleware);
r.use(principal.middleware);
/**
/**
@@ -42,7 +42,7 @@ function homeJsonLd(config) {
@@ -42,7 +42,7 @@ function homeJsonLd(config) {
}
}
/**
/**
- * The pages a crawler may read: this brand's homes, the safety rules and the update log — nothing behind sign-in.
+ * The pages a crawler may read: the home page, the safety rules and the update log — nothing behind sign-in.
* /files, /s/… and /staff are a person's own, so they are disallowed above and left out here.
* /files, /s/… and /staff are a person's own, so they are disallowed above and left out here.
*/
*/
const publicPages = () => [
const publicPages = () => [
@@ -1,8 +1,7 @@
@@ -1,8 +1,7 @@
'use strict';
'use strict';
/**
/**
- * The pages of openvibe.download, plus the honest "coming" homes of openvibe.pics and openvibe.video (the host
- * decides which, see server/brand.js).
+ * The pages of openvibe.download.
*
*
* / what this is, the safety rules and how to sign in
* / what this is, the safety rules and how to sign in
* /files your drive: folders, files, the usage bar, the upload form
* /files your drive: folders, files, the usage bar, the upload form
@@ -42,7 +41,6 @@ const { send } = require('../render/layout');
@@ -42,7 +41,6 @@ const { send } = require('../render/layout');
const rules = require('../drive/rules');
const rules = require('../drive/rules');
const { isStaff } = require('../drive/staff');
const { isStaff } = require('../drive/staff');
const { boundaryOf, readFormData } = require('./form-upload');
const { boundaryOf, readFormData } = require('./form-upload');
-const { brandOf, COMING } = require('../brand');
const SITE_NAME = 'OpenVibe.MediaHub';
const SITE_NAME = 'OpenVibe.MediaHub';
const TAGLINE = 'Your files, shared on your terms.';
const TAGLINE = 'Your files, shared on your terms.';
@@ -142,66 +140,7 @@ ${notice(p && p.detail ? p.detail : 'You are over the limit for this.', 'warn')}
@@ -142,66 +140,7 @@ ${notice(p && p.detail ? p.detail : 'You are over the limit for this.', 'warn')}
body: html`<h1>Sign in first</h1>${signInPrompt(why, back === 'here' ? req.originalUrl : back)}`,
body: html`<h1>Sign in first</h1>${signInPrompt(why, back === 'here' ? req.originalUrl : back)}`,
}, 401));
}, 401));
- // ── The unlaunched brands ────────────────────────────────
- /**
- * openvibe.pics and openvibe.video answer an honest "coming" page and nothing else: no drive, no share links,
- * no uploads. /updates and the crawl artifacts are the same on every brand, and everything else 404s — the
- * product has not shipped there, so there is nothing to offer but the truth.
- */
- const PASS_THROUGH = new Set(['/robots.txt', '/sitemap.xml', '/llms.txt', '/llms-full.txt', '/release.json', '/auth/me', '/favicon.ico']);
- r.use((req, res, next) => {
- if (brandOf(req) === 'download') return next();
- if (PASS_THROUGH.has(req.path)) return next();
- if (req.path === '/') return next(); // the brand's own home, below
- if (req.path === '/updates') return next();
- const brand = COMING[brandOf(req)];
- return page(req, res, {
- title: `${brand.name} is coming`,
- crumbs: [{ label: 'Home', href: '/' }, { label: 'Coming' }],
- body: html`<h1>${brand.name} is not open yet</h1>
-${notice(`${brand.name} has no uploads and no file pages yet. The only brand of this service that has launched is OpenVibe.Download.`, 'warn')}
-<p class="muted">This host serves ${brand.domain}, one of the three brands of the same OpenVibe.MediaHub service. <a href="https://openvibe.download">Go to OpenVibe.Download</a> for the part that works today.</p>`,
- }, 404);
- });
-
// ── Home ─────────────────────────────────────────────────
// ── Home ─────────────────────────────────────────────────
- const comingBody = (brand) => html`${raw(showcase.hero({
- eyebrow: `${brand.name} · ${brand.tagline}`,
- title: brand.name,
- accent: 'coming soon',
- lede: brand.what,
- actions: [{ label: 'OpenVibe.Download is open', href: 'https://openvibe.download', primary: true }, { label: 'What shipped', href: '/updates' }],
- note: 'Nothing is uploaded here yet and there is no way to sign in on this brand.',
- }))}
-${raw(showcase.features({
- title: 'What it will be',
- lede: 'The shape of it, from the product\'s own plan.',
- items: brand.pillars.map((p) => ({ icon: 'ov:page', title: p, text: '' })),
- }))}
-${raw(showcase.features({
- title: 'Why it is not open yet',
- lede: 'Two things have to be true before OpenVibe runs a public place for other people\'s pictures and videos.',
- items: [
- { icon: 'ov:shield', title: 'Safety tooling first', text: 'There is no malware or CSAM scanning on the network yet, so uploads here wait for it. OpenVibe.Download opens first precisely because it is a private drive whose share links only signed-in people can open.' },
- { icon: 'ov:page', title: 'One service, three brands', text: `${brand.name} is served by the same process as openvibe.download — the same file store, the same account and the same rules. This host answers only this page until that tooling is here.` },
- ],
- }))}
-${raw(showcase.cta({ title: 'In the meantime', text: 'OpenVibe.Media already stores and serves the network\'s pictures and video, and OpenVibe.Download is open for private files.', actions: [{ label: 'OpenVibe.Media', href: 'https://openvibe.media' }, { label: 'OpenVibe.Download', href: 'https://openvibe.download' }] }))}`;
-
- r.get('/', (req, res, next) => {
- const brand = COMING[brandOf(req)];
- if (brand) {
- return page(req, res, {
- index: true, cache: PUBLIC_CACHE, styles: [showcase.STYLESHEET],
- title: `${brand.name} — coming soon`,
- description: `${brand.name}: ${brand.tagline} Not open yet — uploads here wait for safety tooling. OpenVibe.Download is the brand of this service that has launched.`,
- crumbs: [{ label: 'Home', href: '/' }],
- body: comingBody(brand),
- });
- }
- return next();
- });
-
r.get('/', (req, res) => {
r.get('/', (req, res) => {
const hero = showcase.hero({
const hero = showcase.hero({
eyebrow: `${SITE_NAME} · ${TAGLINE}`,
eyebrow: `${SITE_NAME} · ${TAGLINE}`,
deleted file mode 100644
deleted file mode 100644
@@ -1,80 +0,0 @@
@@ -1,80 +0,0 @@
-'use strict';
-/**
- * One process, three brand surfaces (the media-hub product manifest). The Host header decides which one a request
- * is for, and v1 has launched only openvibe.download: openvibe.pics and openvibe.video answer an honest "coming"
- * page and nothing else — no drive, no share links, no API and above all no uploads, because neither brand has the
- * safety tooling its content needs yet.
- */
-const assert = require('assert');
-const { boot, check, done } = require('./helpers/boot');
-const { SAME, bytes, upload, share } = require('./helpers/drive');
-
-const PICS = { 'x-forwarded-host': 'openvibe.pics' };
-const VIDEO = { 'x-forwarded-host': 'openvibe.video' };
-
-(async () => {
- const t = await boot();
- const kim = t.network.addUser('kim');
- const other = t.network.addUser('sam');
- const file = (await upload(t, kim, bytes(120, 'brand'), { name: 'brand.bin' })).json().file;
- const slug = (await share(t, kim, { file_id: file.id, hours: 24 })).json().share.slug;
-
- await check('openvibe.pics and openvibe.video answer a coming home that says what they will be', async () => {
- const pics = await t.get('/', { headers: PICS });
- assert.strictEqual(pics.status, 200);
- assert.match(pics.headers.get('content-type'), /^text\/html/);
- assert.ok(pics.text.includes('OpenVibe.Pics'), 'the brand');
- assert.ok(pics.text.includes('Share pictures. Fast, open, yours.'), 'its own tagline');
- assert.ok(pics.text.includes('Albums & galleries') || pics.text.includes('Albums & galleries'), 'what it will be');
- assert.ok(/safety tooling/i.test(pics.text), 'why it is not open yet');
- assert.ok(/no malware or CSAM scanning/i.test(pics.text), 'said plainly');
- assert.ok(pics.text.includes('https://openvibe.download'), 'a link to the brand that has launched');
-
- const video = await t.get('/', { headers: VIDEO });
- assert.strictEqual(video.status, 200);
- assert.ok(video.text.includes('OpenVibe.Video'));
- assert.ok(video.text.includes('Your videos, everywhere.'));
- assert.ok(/safety tooling/i.test(video.text));
- assert.ok(video.text.includes('https://openvibe.download'));
- });
-
- await check('nothing else exists on those brands: no drive, no share, no API, no upload', async () => {
- const before = t.media.requests.length;
- for (const headers of [PICS, VIDEO]) {
- const brand = headers['x-forwarded-host'];
- for (const p of ['/files', `/files/${file.id}`, `/files/${file.id}/download`, `/s/${slug}`, `/s/${slug}/download`, `/staff`, '/api/v1/ping']) {
- const r = await t.get(p, { as: kim, headers });
- assert.strictEqual(r.status, 404, `${brand} ${p} answered ${r.status}`);
- }
- // The write routes too — with a real file behind them.
- const up = await t.get('/api/v1/files', { as: kim, headers: { ...headers, ...SAME }, json: { name: 'x.bin', size: 10, content_type: 'text/plain' } });
- assert.strictEqual(up.status, 404, `${brand}: the API is not there`);
- const form = await t.get('/files/upload', { as: kim, method: 'POST', headers: { ...headers, ...SAME, 'content-type': 'application/x-www-form-urlencoded' }, body: 'name=x' });
- assert.strictEqual(form.status, 404, `${brand}: no form upload`);
- }
- assert.strictEqual(t.media.requests.length, before, 'and OpenVibe.Media was never called');
- });
-
- await check('the crawl artifacts and the update log are the same on every brand', async () => {
- for (const headers of [{}, PICS, VIDEO]) {
- assert.strictEqual((await t.get('/updates', { headers })).status, 200);
- assert.strictEqual((await t.get('/robots.txt', { headers })).status, 200);
- assert.strictEqual((await t.get('/sitemap.xml', { headers })).status, 200);
- assert.strictEqual((await t.get('/llms.txt', { headers })).status, 200);
- }
- });
-
- await check('the launched brand is untouched, and an unknown host is the launched brand', async () => {
- assert.ok((await t.get('/')).text.includes('OpenVibe.Download'));
- assert.ok((await t.get('/')).text.includes('private drive'));
- for (const headers of [{ 'x-forwarded-host': 'openvibe.download' }, { 'x-forwarded-host': 'www.openvibe.download' }, { 'x-forwarded-host': '127.0.0.1' }, { 'x-forwarded-host': 'openvibe.host' }]) {
- const r = await t.get('/', { headers });
- assert.ok(r.text.includes('OpenVibe.Download'), JSON.stringify(headers));
- }
- assert.strictEqual((await t.get('/files', { as: kim })).status, 200);
- assert.strictEqual((await t.get(`/s/${slug}`, { as: other })).status, 200, 'and its share links work');
- });
-
- await t.close();
- done();
-})().catch((e) => { console.error(e); process.exit(1); });
new file mode 100644
new file mode 100644
@@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
+'use strict';
+// deploy/nginx/openvibe.download.conf: openvibe.pics and openvibe.video are parked. Their server blocks (HTTP and
+// HTTPS, apex and www) answer 302 to openvibe.network and proxy nothing — no request for either host reaches the
+// app, which is why the app has no "coming" pages for them. Each name keeps its own certificate.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const conf = fs.readFileSync(path.join(__dirname, '..', ...'deploy/nginx/openvibe.download.conf'.split('/')), 'utf8');
+const blocks = [...conf.matchAll(/(?:^|\n)server \{([\s\S]*?)\n\}/g)].map((m) => m[1]);
+const named = (host) => blocks.filter((b) => new RegExp(`server_name [^;]*\\b${host.replace(/\./g, '\\.')}\\b`).test(b));
+
+for (const host of ['openvibe.pics', 'openvibe.video']) {
+ const parked = named(host);
+ assert.ok(parked.length >= 3, `${host}: the HTTP block and the apex and www HTTPS blocks are there`);
+ for (const b of parked) {
+ assert.match(b, /return 302 https:\/\/openvibe\.network\/;/, `${host}: redirects to openvibe.network`);
+ assert.doesNotMatch(b, /proxy_pass/, `${host}: proxies nothing`);
+ }
+}
+
+// Each host uses its own certificate, on both its apex and its www block.
+for (const host of ['openvibe.pics', 'openvibe.video']) {
+ for (const b of named(host).filter((x) => /listen 443 ssl;/.test(x))) {
+ assert.match(b, new RegExp(`ssl_certificate\\s+/etc/letsencrypt/live/${host.replace(/\./g, '\\.')}/fullchain\\.pem;`), `${host}: its own certificate`);
+ }
+}
+
+console.log('nginx parked domains: pics and video answer 302 openvibe.network, no proxy_pass, own certificates');