/** * Klonkt Beta โ€” server bootstrap * * Personal multi-site platform โ€” Node + SQLite + htmx. * Stack: Express + better-sqlite3 + EJS + htmx + ws. */ import 'dotenv/config'; import express from 'express'; import helmet from 'helmet'; import session from 'express-session'; import bodyParser from 'body-parser'; import path from 'path'; import fs from 'fs'; import crypto from 'crypto'; import { fileURLToPath } from 'url'; import http from 'http'; import db, { initializeDatabase } from './config/database.js'; import { startScheduler } from './services/Scheduler.js'; import { SqliteSessionStore } from './services/SqliteSessionStore.js'; import { ensurePrimarySite } from './services/ensurePrimarySite.js'; import { getThumbnail, getRemoteThumbnail, verifyImg, THUMB_SIZES } from './services/ThumbnailService.js'; import { resolveSite, loadAudioTracks, loadTheme } from './middleware/site.js'; import { isViewer } from './middleware/auth.js'; import { renderPage } from './middleware/render.js'; import { audioEnabled } from './config/features.js'; import authRoutes from './routes/auth.js'; import accountRoutes from './routes/account.js'; import adminRoutes from './routes/admin.js'; import adminAudioRoutes from './routes/admin-audio.js'; import adminPlaylistsRoutes from './routes/admin-playlists.js'; import adminListenersRoutes from './routes/admin-listeners.js'; import adminSitesRoutes from './routes/admin-sites.js'; import adminUsersRoutes from './routes/admin-users.js'; import adminSettingsRoutes from './routes/admin-settings.js'; import adminSeoRoutes from './routes/admin-seo.js'; import audioRoutes from './routes/audio.js'; import searchRoutes from './routes/search.js'; import tagsRoutes from './routes/tags.js'; import typesRoutes from './routes/types.js'; import usersRoutes from './routes/users.js'; import feedRoutes from './routes/feed.js'; import postsRoutes from './routes/posts.js'; import paidRoutes from './routes/paid.js'; import langRoutes from './routes/lang.js'; import adminUpdatesRoutes from './routes/admin-updates.js'; import adminPatreonRoutes from './routes/admin-patreon.js'; import adminStatsRoutes from './routes/admin-stats.js'; import adminPaidRoutes from './routes/admin-paid.js'; import adminPushRoutes from './routes/admin-push.js'; import pushRoutes from './routes/push.js'; import guardianRoutes from './routes/guardian.js'; import adminMediaRoutes from './routes/admin-media.js'; import adminMigrateRoutes from './routes/admin-migrate.js'; import circleRoutes from './routes/circle.js'; import epkRoutes from './routes/epk.js'; import newsletterRoutes from './routes/newsletter.js'; import adminNewsletterRoutes from './routes/admin-newsletter.js'; import downloadRoutes from './routes/download.js'; import linkbioRoutes from './routes/linkbio.js'; import embedRoutes from './routes/embed.js'; import showsRoutes from './routes/shows.js'; import adminShowsRoutes from './routes/admin-shows.js'; import adminEpkRoutes from './routes/admin-epk.js'; import changelogRoutes from './routes/changelog.js'; import ogRoutes from './routes/og.js'; import apRoutes from './routes/activitypub.js'; import oauthRoutes from './routes/oauth.js'; import { apWants, startDeliveryWorker, selfHealTimeline, migrateReactions } from './services/ActivityPubService.js'; // SESSION_SECRET: use the env var if set. Otherwise auto-generate a strong one // and persist it next to the database, so it stays stable across restarts and // updates. This lets Docker / bare-Node installs run with zero manual config. if (!process.env.SESSION_SECRET) { const dataDir = path.dirname(process.env.DATABASE_PATH || './storage/database.sqlite'); const secretFile = path.join(dataDir, '.session-secret'); try { process.env.SESSION_SECRET = fs.readFileSync(secretFile, 'utf8').trim(); } catch { /* not yet generated */ } if (!process.env.SESSION_SECRET) { fs.mkdirSync(dataDir, { recursive: true }); process.env.SESSION_SECRET = crypto.randomBytes(32).toString('hex'); fs.writeFileSync(secretFile, process.env.SESSION_SECRET, { mode: 0o600 }); console.log(`๐Ÿ”‘ Generated a SESSION_SECRET (stored in ${secretFile})`); } } // A SESSION_SECRET that was explicitly set in the env must still be strong in prod. if (process.env.NODE_ENV === 'production' && process.env.SESSION_SECRET.length < 32) { console.error('โŒ FATAL: SESSION_SECRET is too weak for production (set a longer, random one in .env)'); process.exit(1); } const __dirname = path.dirname(fileURLToPath(import.meta.url)); const PORT = process.env.PORT || 3000; // Interface to bind. Default 0.0.0.0 (needed for Docker port-forwarding). Behind a // reverse proxy on the same host, set HOST=127.0.0.1 so the app is NOT reachable // directly from the internet (only via the proxy) โ€” see README/install docs. const HOST = process.env.HOST || '0.0.0.0'; const isDev = process.env.NODE_ENV !== 'production'; const app = express(); const server = http.createServer(app); // Per-request CSP nonce for the strict script-src (nonce + strict-dynamic). Must be set // before helmet builds the CSP header below. The nonce is injected into every