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