Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
1import { Elysia } from 'elysia' 2import type { Context } from 'elysia' 3import { cors } from '@elysiajs/cors' 4import { staticPlugin } from '@elysiajs/static' 5 6import type { Config } from './lib/types' 7import { BASE_HOST } from './lib/constants' 8import { 9 createClientMetadata, 10 getOAuthClient, 11 getCurrentKeys, 12 cleanupExpiredSessions, 13 rotateKeysIfNeeded 14} from './lib/oauth-client' 15import { authRoutes } from './routes/auth' 16import { wispRoutes } from './routes/wisp' 17import { domainRoutes } from './routes/domain' 18import { userRoutes } from './routes/user' 19import { siteRoutes } from './routes/site' 20import { csrfProtection } from './lib/csrf' 21import { DNSVerificationWorker } from './lib/dns-verification-worker' 22import { logger, logCollector, observabilityMiddleware } from './lib/observability' 23import { promptAdminSetup } from './lib/admin-auth' 24import { adminRoutes } from './routes/admin' 25import { previewRoutes } from './routes/preview' 26 27const config: Config = { 28 domain: (Bun.env.DOMAIN ?? `https://${BASE_HOST}`) as Config['domain'], 29 clientName: Bun.env.CLIENT_NAME ?? 'PDS-View' 30} 31 32// Initialize admin setup (prompt if no admin exists) 33await promptAdminSetup() 34 35const client = await getOAuthClient(config) 36 37// Periodic maintenance: cleanup expired sessions and rotate keys 38// Run every hour 39const runMaintenance = async () => { 40 console.log('[Maintenance] Running periodic maintenance...') 41 await cleanupExpiredSessions() 42 await rotateKeysIfNeeded() 43} 44 45// Run maintenance on startup 46runMaintenance() 47 48// Schedule maintenance to run every hour 49setInterval(runMaintenance, 60 * 60 * 1000) 50 51// Start DNS verification worker (runs every 10 minutes) 52const dnsVerifier = new DNSVerificationWorker( 53 10 * 60 * 1000, // 10 minutes 54 (msg, data) => { 55 logCollector.info(`[DNS Verifier] ${msg}`, 'main-app', data ? { data } : undefined) 56 } 57) 58 59dnsVerifier.start() 60logger.info('DNS Verifier Started - checking custom domains every 10 minutes') 61 62export const app = new Elysia() 63 .server({ 64 development: Bun.env.NODE_ENV !== 'production', 65 }) 66 .serve({ 67 hmr: Bun.env.NODE_ENV !== 'production' 68 }) 69 // Observability middleware 70 .onBeforeHandle(observabilityMiddleware('main-app').beforeHandle) 71 .onAfterHandle((ctx: Context) => { 72 observabilityMiddleware('main-app').afterHandle(ctx) 73 // Security headers middleware 74 const { set } = ctx 75 // Prevent clickjacking attacks 76 set.headers['X-Frame-Options'] = 'DENY' 77 // Prevent MIME type sniffing 78 set.headers['X-Content-Type-Options'] = 'nosniff' 79 // Strict Transport Security (HSTS) - enforce HTTPS 80 set.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' 81 // Referrer policy - limit referrer information 82 set.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' 83 // Content Security Policy 84 set.headers['Content-Security-Policy'] = 85 "default-src 'self'; " + 86 "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " + 87 "style-src 'self' 'unsafe-inline'; " + 88 "img-src 'self' data: https:; " + 89 "font-src 'self' data:; " + 90 "connect-src 'self' https:; " + 91 "frame-ancestors 'none'; " + 92 "base-uri 'self'; " + 93 "form-action 'self'" 94 // Additional security headers 95 set.headers['X-XSS-Protection'] = '1; mode=block' 96 set.headers['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()' 97 }) 98 .onError(observabilityMiddleware('main-app').onError) 99 .use(csrfProtection()) 100 .use(authRoutes(client)) 101 .use(wispRoutes(client)) 102 .use(domainRoutes(client)) 103 .use(userRoutes(client)) 104 .use(siteRoutes(client)) 105 .use(adminRoutes()) 106 .use(previewRoutes) 107 .use( 108 await staticPlugin({ 109 prefix: '/' 110 }) 111 ) 112 .get('/client-metadata.json', () => { 113 return createClientMetadata(config) 114 }) 115 .get('/jwks.json', async () => { 116 const keys = await getCurrentKeys() 117 if (!keys.length) return { keys: [] } 118 119 return { 120 keys: keys.map((k) => { 121 const jwk = k.publicJwk ?? k 122 const { ...pub } = jwk 123 return pub 124 }) 125 } 126 }) 127 .get('/api/health', () => { 128 const dnsVerifierHealth = dnsVerifier.getHealth() 129 return { 130 status: 'ok', 131 timestamp: new Date().toISOString(), 132 dnsVerifier: dnsVerifierHealth 133 } 134 }) 135 .get('/api/admin/test', () => { 136 return { message: 'Admin routes test works!' } 137 }) 138 .post('/api/admin/verify-dns', async () => { 139 try { 140 await dnsVerifier.trigger() 141 return { 142 success: true, 143 message: 'DNS verification triggered' 144 } 145 } catch (error) { 146 return { 147 success: false, 148 error: error instanceof Error ? error.message : String(error) 149 } 150 } 151 }) 152 .use(cors({ 153 origin: config.domain, 154 credentials: true, 155 methods: ['GET', 'POST', 'DELETE', 'PUT', 'PATCH', 'OPTIONS'], 156 allowedHeaders: ['Content-Type', 'Authorization', 'Origin', 'X-Forwarded-Host'], 157 exposeHeaders: ['Content-Type'], 158 maxAge: 86400 // 24 hours 159 })) 160 .listen(8000) 161 162console.log( 163 `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` 164)