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