openvibers/openvibe.mediahub Commits

MediaHub: openvibe.pics and openvibe.video are parked, not coming pages (plan rule: no placeholders) (#4)

OP OpenVibers committed 9 Oct 2026 · 2c133e22db · 11 files, +65 −270

- deploy/nginx/openvibe.download.conf: both names (and www.) answer 302 to openvibe.network and proxy nothing, each with
  its own certificate. openvibe.download is unchanged.
- The coming pages are deleted: server/brand.js (COMING, brandOf, the middleware), the pages and API special cases.
- test/nginx-parked-domains.test.js checks the vhost; test/brands.test.js is gone. README, STATUS.json and CHANGELOG
  say the two domains are parked until Pics and Video are built.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 57f45e2..8f8192d 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -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
ddiff --git a/README.md b/README.md
iindex ef5ebc7..3759a5f 100644
---- a/README.md
++++ b/README.md
@@@ -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)
ddiff --git a/STATUS.json b/STATUS.json
iindex 45b0207..b172c43 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -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"
],
ddiff --git a/deploy/nginx/openvibe.download.conf b/deploy/nginx/openvibe.download.conf
iindex 470b18e..832fac2 100644
---- a/deploy/nginx/openvibe.download.conf
++++ b/deploy/nginx/openvibe.download.conf
@@@ -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/;
}
ddiff --git a/server/app.js b/server/app.js
iindex 56893a0..4a4036b 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -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 });
ddiff --git a/server/brand.js b/server/brand.js
ddeleted file mode 100644
iindex d9e95d8..0000000
---- a/server/brand.js
++++ /dev/null
@@@ -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 };
ddiff --git a/server/http/api.js b/server/http/api.js
iindex 3c7802b..a356b1b 100644
---- a/server/http/api.js
++++ b/server/http/api.js
@@@ -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);
/**
ddiff --git a/server/http/discovery.js b/server/http/discovery.js
iindex 5523d87..16fb6e1 100644
---- a/server/http/discovery.js
++++ b/server/http/discovery.js
@@@ -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 = () => [
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex f7b42a2..48abdc3 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -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}`,
ddiff --git a/test/brands.test.js b/test/brands.test.js
ddeleted file mode 100644
iindex 40644ce..0000000
---- a/test/brands.test.js
++++ /dev/null
@@@ -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 &amp; 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); });
ddiff --git a/test/nginx-parked-domains.test.js b/test/nginx-parked-domains.test.js
nnew file mode 100644
iindex 0000000..e3a286b
---- /dev/null
++++ b/test/nginx-parked-domains.test.js
@@@ -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');