import http from 'node:http'; import { dirname, join } from 'node:path'; import { existsSync, readFileSync } from 'node:fs'; import { mkdir, rename, rm, writeFile } from 'node:fs/promises'; const host = process.env.MAINTENANCE_HOST || '127.0.0.1'; const port = Number.parseInt(process.env.PORT || '43821', 10); const dataDir = process.env.MAINTENANCE_DATA_DIR || dirname(process.env.DATABASE_PATH || '/var/lib/synapsis/synapsis.db'); const brandingPath = join(dataDir, 'maintenance-branding.json'); const logoPath = join(dataDir, 'maintenance-logo'); function validAccent(value) { return typeof value === 'string' && /^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(value) ? value : '#9fdf5f'; } async function captureBranding() { const baseUrl = `http://127.0.0.1:${port}`; await mkdir(dataDir, { recursive: true }); const nodeResponse = await fetch(`${baseUrl}/api/node`, { cache: 'no-store' }); if (!nodeResponse.ok) throw new Error(`Node branding returned HTTP ${nodeResponse.status}`); const node = await nodeResponse.json(); const branding = { accentColor: validAccent(node.accentColor), externalLogoUrl: typeof node.logoUrl === 'string' && /^https?:\/\//i.test(node.logoUrl) ? node.logoUrl : null, logoContentType: null, }; const logoResponse = await fetch(`${baseUrl}/api/node/logo`, { cache: 'no-store' }); if (logoResponse.ok) { const contentType = logoResponse.headers.get('content-type')?.split(';')[0]; if (contentType?.startsWith('image/')) { const logoTempPath = `${logoPath}.tmp`; await writeFile(logoTempPath, Buffer.from(await logoResponse.arrayBuffer())); await rename(logoTempPath, logoPath); branding.logoContentType = contentType; } else { await rm(logoPath, { force: true }); } } else { await rm(logoPath, { force: true }); } const brandingTempPath = `${brandingPath}.tmp`; await writeFile(brandingTempPath, `${JSON.stringify(branding)}\n`, { mode: 0o640 }); await rename(brandingTempPath, brandingPath); } if (process.argv.includes('--capture-branding')) { await captureBranding(); process.exit(0); } let branding = {}; try { branding = JSON.parse(readFileSync(brandingPath, 'utf8')); } catch {} const accentColor = validAccent(branding.accentColor); const logoContentType = typeof branding.logoContentType === 'string' && branding.logoContentType.startsWith('image/') ? branding.logoContentType : 'application/octet-stream'; const externalLogoUrl = typeof branding.externalLogoUrl === 'string' && /^https?:\/\//i.test(branding.externalLogoUrl) ? branding.externalLogoUrl : null; const hasStoredLogo = existsSync(logoPath); const logoMarkup = hasStoredLogo || externalLogoUrl ? '' : '
Synapsis
'; const page = ` Update in progress
${logoMarkup}

Update in progress

We’re making Synapsis better. We’ll be right back!

Checking for the node…
`; const server = http.createServer((request, response) => { if (request.url?.split('?')[0] === '/maintenance-logo') { if (hasStoredLogo) { const logo = readFileSync(logoPath); response.writeHead(200, { 'Content-Type': logoContentType, 'Content-Length': logo.byteLength, 'Cache-Control': 'no-store, max-age=0', }); response.end(logo); return; } if (externalLogoUrl) { response.writeHead(302, { Location: externalLogoUrl, 'Cache-Control': 'no-store, max-age=0' }); response.end(); return; } } response.writeHead(503, { 'Content-Type': 'text/html; charset=utf-8', 'Content-Length': Buffer.byteLength(page), 'Cache-Control': 'no-store, max-age=0', 'Retry-After': '30', 'X-Robots-Tag': 'noindex', }); if (request.method === 'HEAD') response.end(); else response.end(page); }); server.listen(port, host, () => { console.log(`Synapsis maintenance page listening on http://${host}:${port}`); }); const shutdown = () => server.close(() => process.exit(0)); process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown);