Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
at redirects 5.6 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 { getCookieSecret } from './lib/db' 16import { authRoutes } from './routes/auth' 17import { wispRoutes } from './routes/wisp' 18import { domainRoutes } from './routes/domain' 19import { userRoutes } from './routes/user' 20import { siteRoutes } from './routes/site' 21import { csrfProtection } from './lib/csrf' 22import { DNSVerificationWorker } from './lib/dns-verification-worker' 23import { logger, logCollector, observabilityMiddleware } from './lib/observability' 24import { promptAdminSetup } from './lib/admin-auth' 25import { adminRoutes } from './routes/admin' 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 35// Get or generate cookie signing secret 36const cookieSecret = await getCookieSecret() 37 38const client = await getOAuthClient(config) 39 40// Periodic maintenance: cleanup expired sessions and rotate keys 41// Run every hour 42const runMaintenance = async () => { 43 console.log('[Maintenance] Running periodic maintenance...') 44 await cleanupExpiredSessions() 45 await rotateKeysIfNeeded() 46} 47 48// Run maintenance on startup 49runMaintenance() 50 51// Schedule maintenance to run every hour 52setInterval(runMaintenance, 60 * 60 * 1000) 53 54// Start DNS verification worker (runs every 10 minutes) 55const dnsVerifier = new DNSVerificationWorker( 56 10 * 60 * 1000, // 10 minutes 57 (msg, data) => { 58 logCollector.info(`[DNS Verifier] ${msg}`, 'main-app', data ? { data } : undefined) 59 } 60) 61 62dnsVerifier.start() 63logger.info('DNS Verifier Started - checking custom domains every 10 minutes') 64 65export const app = new Elysia({ 66 serve: { 67 maxRequestBodySize: 1024 * 1024 * 128 * 3, 68 development: Bun.env.NODE_ENV !== 'production' ? true : false, 69 id: Bun.env.NODE_ENV !== 'production' ? undefined : null, 70 }, 71 cookie: { 72 secrets: cookieSecret, 73 sign: ['did'] 74 } 75 }) 76 // Observability middleware 77 .onBeforeHandle(observabilityMiddleware('main-app').beforeHandle) 78 .onAfterHandle((ctx: Context) => { 79 observabilityMiddleware('main-app').afterHandle(ctx) 80 // Security headers middleware 81 const { set } = ctx 82 // Prevent clickjacking attacks 83 set.headers['X-Frame-Options'] = 'DENY' 84 // Prevent MIME type sniffing 85 set.headers['X-Content-Type-Options'] = 'nosniff' 86 // Strict Transport Security (HSTS) - enforce HTTPS 87 set.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' 88 // Referrer policy - limit referrer information 89 set.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' 90 // Content Security Policy 91 set.headers['Content-Security-Policy'] = 92 "default-src 'self'; " + 93 "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " + 94 "style-src 'self' 'unsafe-inline'; " + 95 "img-src 'self' data: https:; " + 96 "font-src 'self' data:; " + 97 "connect-src 'self' https:; " + 98 "frame-ancestors 'none'; " + 99 "base-uri 'self'; " + 100 "form-action 'self'" 101 // Additional security headers 102 set.headers['X-XSS-Protection'] = '1; mode=block' 103 set.headers['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()' 104 }) 105 .onError(observabilityMiddleware('main-app').onError) 106 .use(csrfProtection()) 107 .use(authRoutes(client, cookieSecret)) 108 .use(wispRoutes(client, cookieSecret)) 109 .use(domainRoutes(client, cookieSecret)) 110 .use(userRoutes(client, cookieSecret)) 111 .use(siteRoutes(client, cookieSecret)) 112 .use(adminRoutes(cookieSecret)) 113 .use( 114 await staticPlugin({ 115 prefix: '/' 116 }) 117 ) 118 .get('/client-metadata.json', () => { 119 return createClientMetadata(config) 120 }) 121 .get('/jwks.json', async ({ set }) => { 122 // Prevent caching to ensure clients always get fresh keys after rotation 123 set.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0' 124 set.headers['Pragma'] = 'no-cache' 125 set.headers['Expires'] = '0' 126 127 const keys = await getCurrentKeys() 128 if (!keys.length) return { keys: [] } 129 130 return { 131 keys: keys.map((k) => { 132 const jwk = k.publicJwk ?? k 133 const { ...pub } = jwk 134 return pub 135 }) 136 } 137 }) 138 .get('/api/health', () => { 139 const dnsVerifierHealth = dnsVerifier.getHealth() 140 return { 141 status: 'ok', 142 timestamp: new Date().toISOString(), 143 dnsVerifier: dnsVerifierHealth 144 } 145 }) 146 .get('/api/admin/test', () => { 147 return { message: 'Admin routes test works!' } 148 }) 149 .post('/api/admin/verify-dns', async () => { 150 try { 151 await dnsVerifier.trigger() 152 return { 153 success: true, 154 message: 'DNS verification triggered' 155 } 156 } catch (error) { 157 return { 158 success: false, 159 error: error instanceof Error ? error.message : String(error) 160 } 161 } 162 }) 163 .get('/.well-known/atproto-did', ({ set }) => { 164 // Return plain text DID for AT Protocol domain verification 165 set.headers['Content-Type'] = 'text/plain' 166 return 'did:plc:7puq73yz2hkvbcpdhnsze2qw' 167 }) 168 .use(cors({ 169 origin: config.domain, 170 credentials: true, 171 methods: ['GET', 'POST', 'DELETE', 'PUT', 'PATCH', 'OPTIONS'], 172 allowedHeaders: ['Content-Type', 'Authorization', 'Origin', 'X-Forwarded-Host'], 173 exposeHeaders: ['Content-Type'], 174 maxAge: 86400 // 24 hours 175 })) 176 .listen(8000) 177 178console.log( 179 `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` 180)