forked from
nekomimi.pet/wisp.place-monorepo
Monorepo for Wisp.place. A static site hosting service built on top of the AT Protocol.
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)