404
This page does not exist
${esc(site.domain)} has only its front page and legal pages for now. The address you followed is not one of them.
`;
}
/** nginx vhost: static root, wildcard cert, www → apex, long cache for the immutable bits, real 404s. */
const SECURITY_HEADERS = [
'add_header X-Content-Type-Options nosniff always;',
'add_header X-Frame-Options SAMEORIGIN always;',
'add_header Referrer-Policy strict-origin-when-cross-origin always;',
'add_header Strict-Transport-Security "max-age=31536000" always;',
];
function vhost(site) {
const d = site.domain;
const sec = SECURITY_HEADERS.join('\n ');
// Subdomains of a zone (e.g. events.openvibe.network) use the zone's wildcard cert and have no www.
const cert = site.zone || d;
const www = site.zone ? '' : `
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name www.${d};
ssl_certificate /etc/letsencrypt/live/${cert}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${cert}/privkey.pem;
return 301 https://${d}$request_uri;
}
`;
return `# ${d} — static front page (OpenVibe.Sites). Generated by build.js; edit notices.json, not this.
server {
listen 80;
listen [::]:80;
server_name ${d}${site.zone ? '' : ` www.${d}`};
return 301 https://${d}$request_uri;
}
${www}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name ${d};
include snippets/security-txt.conf; # /.well-known/security.txt (host file; SECURITY.md in every repo)
ssl_certificate /etc/letsencrypt/live/${cert}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${cert}/privkey.pem;
root /opt/openvibe.sites/dist/${d};
index index.html;
access_log /var/log/nginx/${d}.access.log;
error_log /var/log/nginx/${d}.error.log;
${SECURITY_HEADERS.join('\n ')}
gzip on;
gzip_types text/html text/plain text/css application/json application/javascript application/xml image/svg+xml application/manifest+json;
# An add_header inside a location replaces the server's list (nginx does not merge them), so every
# location that sets its own header repeats the security headers.
location = /robots.txt {
add_header Cache-Control "public, max-age=3600";
${sec}
}
location = /sitemap.xml {
types { application/xml xml; }
add_header Cache-Control "public, max-age=3600";
${sec}
}
location = /manifest.webmanifest {
types { application/manifest+json webmanifest; }
add_header Cache-Control "public, max-age=86400";
${sec}
}
location = /status.json {
add_header Cache-Control "public, max-age=600";
add_header Access-Control-Allow-Origin "*";
${sec}
}
location = /release.json {
add_header Cache-Control "no-cache";
add_header Access-Control-Allow-Origin "*";
${sec}
}
# This repository's pinned shared browser files (navbar, footer, theme loader…), one copy for every Sites
# vhost. Pages ask for them content-addressed (?v=); the few loaded without it (navbar's companions) get
# the same hour.
location ^~ /shared/ {
alias /opt/openvibe.sites/dist/_shared/;
add_header Cache-Control "public, max-age=3600";
add_header Access-Control-Allow-Origin "*";
${sec}
}
# Only the files in dist/ exist; anything else is a real 404 with a page, never the front page.
error_page 404 /404.html;
location = /404.html { internal; }
location / {
add_header Cache-Control "public, max-age=600";
${sec}
try_files $uri $uri.html $uri/ =404;
}
location ~ /\\.(?!well-known) { deny all; }
}
`;
}
function releaseJson(site, html) {
const release = crypto.createHash('sha256').update(String(html).replace(/\d{4}-\d{2}-\d{2}/g, 'DATE')).digest('hex').slice(0, 12);
return JSON.stringify({
service: site.tld, release, released_at: `${today}T00:00:00.000Z`, booted_at: null,
contracts_version: null, packages: { 'openvibe-shared': require('openvibe-shared/package.json').version },
min_client_release: null, mixed_version_window_hours: 24, kind: 'notice',
}, null, 2) + '\n';
}
function statusJson(site) {
return JSON.stringify({
domain: site.domain, name: site.name, stage: site.kind, live: false,
network: NET.networkUrl, updated: today,
note: 'Static notice served by OpenVibers/OpenVibe.Sites. See the linked destination for current information.',
}, null, 2) + '\n';
}
function build() {
const out = {};
for (const site of notices) {
out[`${site.domain}/index.html`] = page(site);
out[`${site.domain}/404.html`] = notFound(site);
out[`${site.domain}/robots.txt`] = robots(site);
out[`${site.domain}/manifest.webmanifest`] = manifest(site);
out[`${site.domain}/release.json`] = releaseJson(site, out[`${site.domain}/index.html`]);
out[`${site.domain}/status.json`] = statusJson(site);
for (const kind of ['terms', 'privacy', 'dmca']) out[`${site.domain}/${kind}.html`] = legal.page(kind, legalSite(site));
if (!site.vhostOwner) out[`../deploy/nginx/${site.domain}.conf`] = vhost(site);
}
for (const name of sharedFiles.BROWSER) out[`_shared/${name}`] = fs.readFileSync(sharedFiles.path(name), 'utf8');
return out;
}
function verifyFrozen() {
const noticeDomains = new Set(notices.map(site => site.domain));
let missing = 0;
for (const domain of frozen) {
if (noticeDomains.has(domain)) { console.error(`frozen domain is also a notice: ${domain}`); missing++; }
for (const file of [`dist/${domain}/index.html`, `deploy/nginx/${domain}.conf`]) {
if (!fs.existsSync(path.join(ROOT, file))) { console.error(`missing frozen file: ${file}`); missing++; }
}
}
return missing;
}
function check() {
let stale = verifyFrozen();
const norm = (s) => String(s).replace(/\d{4}-\d{2}-\d{2}/g, 'DATE');
for (const [rel, content] of Object.entries(build())) {
const p = path.join(DIST, rel);
if (!fs.existsSync(p) || norm(fs.readFileSync(p, 'utf8')) !== norm(content)) { console.error(`stale: ${rel}`); stale++; }
}
return stale;
}
if (require.main === module) {
if (process.argv.includes('--check')) process.exit(check() ? 1 : 0);
for (const [rel, content] of Object.entries(build())) {
const p = path.join(DIST, rel);
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, content);
}
console.log(`built ${notices.length} notices; preserved ${frozen.length} frozen product domains`);
}
module.exports = { build, check, verifyFrozen };