Monorepo for Wisp.place. A static site hosting service built on top of the AT Protocol.
1import { Elysia } from 'elysia' 2import { cors } from '@elysiajs/cors' 3import { staticPlugin } from '@elysiajs/static' 4import { openapi, fromTypes } from '@elysiajs/openapi' 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 { csrfProtection } from './lib/csrf' 20 21const config: Config = { 22 domain: (Bun.env.DOMAIN ?? `https://${BASE_HOST}`) as `https://${string}`, 23 clientName: Bun.env.CLIENT_NAME ?? 'PDS-View' 24} 25 26const client = await getOAuthClient(config) 27 28// Periodic maintenance: cleanup expired sessions and rotate keys 29// Run every hour 30const runMaintenance = async () => { 31 console.log('[Maintenance] Running periodic maintenance...') 32 await cleanupExpiredSessions() 33 await rotateKeysIfNeeded() 34} 35 36// Run maintenance on startup 37runMaintenance() 38 39// Schedule maintenance to run every hour 40setInterval(runMaintenance, 60 * 60 * 1000) 41 42export const app = new Elysia() 43 // Security headers middleware 44 .onAfterHandle(({ set }) => { 45 // Prevent clickjacking attacks 46 set.headers['X-Frame-Options'] = 'DENY' 47 // Prevent MIME type sniffing 48 set.headers['X-Content-Type-Options'] = 'nosniff' 49 // Strict Transport Security (HSTS) - enforce HTTPS 50 set.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' 51 // Referrer policy - limit referrer information 52 set.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' 53 // Content Security Policy 54 set.headers['Content-Security-Policy'] = 55 "default-src 'self'; " + 56 "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " + 57 "style-src 'self' 'unsafe-inline'; " + 58 "img-src 'self' data: https:; " + 59 "font-src 'self' data:; " + 60 "connect-src 'self' https:; " + 61 "frame-ancestors 'none'; " + 62 "base-uri 'self'; " + 63 "form-action 'self'" 64 // Additional security headers 65 set.headers['X-XSS-Protection'] = '1; mode=block' 66 set.headers['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()' 67 }) 68 .use( 69 openapi({ 70 references: fromTypes() 71 }) 72 ) 73 .use( 74 await staticPlugin({ 75 prefix: '/' 76 }) 77 ) 78 .use(csrfProtection()) 79 .use(authRoutes(client)) 80 .use(wispRoutes(client)) 81 .use(domainRoutes(client)) 82 .use(userRoutes(client)) 83 .get('/client-metadata.json', (c) => { 84 return createClientMetadata(config) 85 }) 86 .get('/jwks.json', (c) => { 87 const keys = getCurrentKeys() 88 if (!keys.length) return { keys: [] } 89 90 return { 91 keys: keys.map((k) => { 92 const jwk = k.publicJwk ?? k 93 const { ...pub } = jwk 94 return pub 95 }) 96 } 97 }) 98 .use(cors({ 99 origin: config.domain, 100 credentials: true, 101 methods: ['GET', 'POST', 'DELETE', 'PUT', 'PATCH', 'OPTIONS'], 102 allowedHeaders: ['Content-Type', 'Authorization', 'Origin', 'X-Forwarded-Host'], 103 exposeHeaders: ['Content-Type'], 104 maxAge: 86400 // 24 hours 105 })) 106 .listen(8000) 107 108console.log( 109 `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` 110)