// 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 ` Protected
🔒

Protected

Enter the passphrase to view this page.

${err ? 'Incorrect passphrase.' : ''}
`; } // Every response must be uncacheable — otherwise the CDN could cache a gated file // under its URL and serve it to unauthenticated visitors (bypassing the gate), or // cache a lock page and show it to authenticated ones. const NOCACHE = { 'Cache-Control': 'no-store, no-cache, must-revalidate, private', 'Pragma': 'no-cache', 'Vary': 'Cookie' }; function send(res, code, type, body, extra) { const h = { 'Content-Type': type, 'X-Robots-Tag': 'noindex, nofollow', ...NOCACHE, ...(extra || {}) }; res.writeHead(code, h); res.end(body); } http.createServer((req, res) => { const u = new URL(req.url, 'http://x'); let pathname = decodeURIComponent(u.pathname); if (pathname === '/__gate' && req.method === 'POST') { let b = ''; req.on('data', (d) => { b += d; if (b.length > 4096) req.destroy(); }); req.on('end', () => { const p = new URLSearchParams(b); const next = safeNext(p.get('next')); let ok = false; try { ok = crypto.timingSafeEqual(Buffer.from(p.get('pass') || ''), Buffer.from(PASSPHRASE)); } catch (_) {} if (ok) { send(res, 302, 'text/plain', 'ok', { 'Set-Cookie': `${COOKIE}=${TOKEN}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=${MAXAGE}`, Location: next, }); } else { send(res, 200, 'text/html; charset=utf-8', lockPage(true, next)); } }); return; } if (!authed(req)) { send(res, 200, 'text/html; charset=utf-8', lockPage(false, pathname)); return; } // Authed: serve the file from disk. let rel = pathname.replace(/^\/+/, ''); let abs = path.normalize(path.join(ROOT, rel)); if (!abs.startsWith(ROOT)) { send(res, 403, 'text/plain', 'forbidden'); return; } try { let st = fs.statSync(abs); if (st.isDirectory()) { abs = path.join(abs, 'index.html'); st = fs.statSync(abs); } const base = path.basename(abs); if (BLOCK.has(base) || abs.includes('/node_modules/') || abs.includes('/.git/')) { send(res, 404, 'text/plain', 'not found'); return; } const type = MIME[path.extname(abs).toLowerCase()] || 'application/octet-stream'; res.writeHead(200, { 'Content-Type': type, 'X-Robots-Tag': 'noindex, nofollow', ...NOCACHE }); fs.createReadStream(abs).pipe(res); } catch (_) { send(res, 404, 'text/html; charset=utf-8', '

404

'); } }).listen(PORT, () => console.log('gate proxy on ' + PORT));