// Reusable Mosaic passphrase gate (server-side). Drop this + package.json into any // static Mosaic project; it serves the project's existing files (HTML, PDF, assets) // but only after a shared passphrase is entered. Gates EVERY route (unlike a // client-side gate, PDFs and assets are protected too) and marks everything noindex. // Stdlib only — no npm deps to install. const http = require('http'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const ROOT = __dirname; const PORT = process.env.PORT || 8080; const PASSPHRASE = process.env.GATE_PASSPHRASE || 'OpenSaysMe'; const SECRET = process.env.GATE_SECRET || PASSPHRASE; // cookie signer const COOKIE = 'mz_gate'; const TOKEN = crypto.createHmac('sha256', SECRET).update('v1').digest('hex'); const MAXAGE = 30 * 24 * 3600; const MIME = { '.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.json': 'application/json', '.pdf': 'application/pdf', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.txt': 'text/plain; charset=utf-8', '.xml': 'application/xml', '.mp4': 'video/mp4', }; const BLOCK = new Set(['server.js', 'package.json', 'package-lock.json']); function authed(req) { const c = (req.headers.cookie || '').split(';').map((s) => s.trim()); for (const kv of c) { const i = kv.indexOf('='); if (i > 0 && kv.slice(0, i) === COOKIE) { const v = kv.slice(i + 1); try { return crypto.timingSafeEqual(Buffer.from(v), Buffer.from(TOKEN)); } catch (_) { return false; } } } return false; } function safeNext(n) { return (typeof n === 'string' && n.startsWith('/') && !n.startsWith('//')) ? n : '/'; } function lockPage(err, next) { return `
Enter the passphrase to view this page.