Monorepo for Wisp.place. A static site hosting service built on top of the AT Protocol.
at main 5.1 kB view raw
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' 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 serve: { 63 maxPayloadLength: 1024 * 1024 * 128 * 3, 64 development: Bun.env.NODE_ENV !== 'production' ? true : false, 65 id: Bun.env.NODE_ENV !== 'production' ? undefined : null, 66 } 67 }) 68 // Observability middleware 69 .onBeforeHandle(observabilityMiddleware('main-app').beforeHandle) 70 .onAfterHandle((ctx: Context) => { 71 observabilityMiddleware('main-app').afterHandle(ctx) 72 // Security headers middleware 73 const { set } = ctx 74 // Prevent clickjacking attacks 75 set.headers['X-Frame-Options'] = 'DENY' 76 // Prevent MIME type sniffing 77 set.headers['X-Content-Type-Options'] = 'nosniff' 78 // Strict Transport Security (HSTS) - enforce HTTPS 79 set.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' 80 // Referrer policy - limit referrer information 81 set.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' 82 // Content Security Policy 83 set.headers['Content-Security-Policy'] = 84 "default-src 'self'; " + 85 "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " + 86 "style-src 'self' 'unsafe-inline'; " + 87 "img-src 'self' data: https:; " + 88 "font-src 'self' data:; " + 89 "connect-src 'self' https:; " + 90 "frame-ancestors 'none'; " + 91 "base-uri 'self'; " + 92 "form-action 'self'" 93 // Additional security headers 94 set.headers['X-XSS-Protection'] = '1; mode=block' 95 set.headers['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()' 96 }) 97 .onError(observabilityMiddleware('main-app').onError) 98 .use(csrfProtection()) 99 .use(authRoutes(client)) 100 .use(wispRoutes(client)) 101 .use(domainRoutes(client)) 102 .use(userRoutes(client)) 103 .use(siteRoutes(client)) 104 .use(adminRoutes()) 105 .use( 106 await staticPlugin({ 107 prefix: '/' 108 }) 109 ) 110 .get('/client-metadata.json', () => { 111 return createClientMetadata(config) 112 }) 113 .get('/jwks.json', async () => { 114 const keys = await getCurrentKeys() 115 if (!keys.length) return { keys: [] } 116 117 return { 118 keys: keys.map((k) => { 119 const jwk = k.publicJwk ?? k 120 const { ...pub } = jwk 121 return pub 122 }) 123 } 124 }) 125 .get('/api/health', () => { 126 const dnsVerifierHealth = dnsVerifier.getHealth() 127 return { 128 status: 'ok', 129 timestamp: new Date().toISOString(), 130 dnsVerifier: dnsVerifierHealth 131 } 132 }) 133 .get('/api/admin/test', () => { 134 return { message: 'Admin routes test works!' } 135 }) 136 .post('/api/admin/verify-dns', async () => { 137 try { 138 await dnsVerifier.trigger() 139 return { 140 success: true, 141 message: 'DNS verification triggered' 142 } 143 } catch (error) { 144 return { 145 success: false, 146 error: error instanceof Error ? error.message : String(error) 147 } 148 } 149 }) 150 .get('/.well-known/atproto-did', ({ set }) => { 151 // Return plain text DID for AT Protocol domain verification 152 set.headers['Content-Type'] = 'text/plain' 153 return 'did:plc:7puq73yz2hkvbcpdhnsze2qw' 154 }) 155 .use(cors({ 156 origin: config.domain, 157 credentials: true, 158 methods: ['GET', 'POST', 'DELETE', 'PUT', 'PATCH', 'OPTIONS'], 159 allowedHeaders: ['Content-Type', 'Authorization', 'Origin', 'X-Forwarded-Host'], 160 exposeHeaders: ['Content-Type'], 161 maxAge: 86400 // 24 hours 162 })) 163 .listen(8000) 164 165console.log( 166 `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` 167)