Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
96
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 2a8b0ae5482c30f3b2c41feff92c019943cb3abe 180 lines 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)