Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
wisp.place
1import { Elysia } from 'elysia'
2import { cors } from '@elysiajs/cors'
3import { staticPlugin } from '@elysiajs/static'
4
5import type { Config } from './lib/types'
6import { BASE_HOST } from './lib/constants'
7import {
8 createClientMetadata,
9 getOAuthClient,
10 getCurrentKeys,
11 cleanupExpiredSessions,
12 rotateKeysIfNeeded
13} from './lib/oauth-client'
14import { authRoutes } from './routes/auth'
15import { wispRoutes } from './routes/wisp'
16import { domainRoutes } from './routes/domain'
17import { userRoutes } from './routes/user'
18import { csrfProtection } from './lib/csrf'
19import { DNSVerificationWorker } from './lib/dns-verification-worker'
20import { logger } from './lib/logger'
21
22const config: Config = {
23 domain: (Bun.env.DOMAIN ?? `https://${BASE_HOST}`) as `https://${string}`,
24 clientName: Bun.env.CLIENT_NAME ?? 'PDS-View'
25}
26
27const client = await getOAuthClient(config)
28
29// Periodic maintenance: cleanup expired sessions and rotate keys
30// Run every hour
31const runMaintenance = async () => {
32 console.log('[Maintenance] Running periodic maintenance...')
33 await cleanupExpiredSessions()
34 await rotateKeysIfNeeded()
35}
36
37// Run maintenance on startup
38runMaintenance()
39
40// Schedule maintenance to run every hour
41setInterval(runMaintenance, 60 * 60 * 1000)
42
43// Start DNS verification worker (runs every hour)
44const dnsVerifier = new DNSVerificationWorker(
45 60 * 60 * 1000, // 1 hour
46 (msg, data) => {
47 logger.info('[DNS Verifier]', msg, data || '')
48 }
49)
50
51dnsVerifier.start()
52logger.info('[DNS Verifier] Started - checking custom domains every hour')
53
54export const app = new Elysia()
55 // Security headers middleware
56 .onAfterHandle(({ set }) => {
57 // Prevent clickjacking attacks
58 set.headers['X-Frame-Options'] = 'DENY'
59 // Prevent MIME type sniffing
60 set.headers['X-Content-Type-Options'] = 'nosniff'
61 // Strict Transport Security (HSTS) - enforce HTTPS
62 set.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
63 // Referrer policy - limit referrer information
64 set.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
65 // Content Security Policy
66 set.headers['Content-Security-Policy'] =
67 "default-src 'self'; " +
68 "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
69 "style-src 'self' 'unsafe-inline'; " +
70 "img-src 'self' data: https:; " +
71 "font-src 'self' data:; " +
72 "connect-src 'self' https:; " +
73 "frame-ancestors 'none'; " +
74 "base-uri 'self'; " +
75 "form-action 'self'"
76 // Additional security headers
77 set.headers['X-XSS-Protection'] = '1; mode=block'
78 set.headers['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()'
79 })
80 .use(
81 await staticPlugin({
82 prefix: '/'
83 })
84 )
85 .use(csrfProtection())
86 .use(authRoutes(client))
87 .use(wispRoutes(client))
88 .use(domainRoutes(client))
89 .use(userRoutes(client))
90 .get('/client-metadata.json', (c) => {
91 return createClientMetadata(config)
92 })
93 .get('/jwks.json', async (c) => {
94 const keys = await getCurrentKeys()
95 if (!keys.length) return { keys: [] }
96
97 return {
98 keys: keys.map((k) => {
99 const jwk = k.publicJwk ?? k
100 const { ...pub } = jwk
101 return pub
102 })
103 }
104 })
105 .get('/api/health', () => {
106 const dnsVerifierHealth = dnsVerifier.getHealth()
107 return {
108 status: 'ok',
109 timestamp: new Date().toISOString(),
110 dnsVerifier: dnsVerifierHealth
111 }
112 })
113 .post('/api/admin/verify-dns', async () => {
114 try {
115 await dnsVerifier.trigger()
116 return {
117 success: true,
118 message: 'DNS verification triggered'
119 }
120 } catch (error) {
121 return {
122 success: false,
123 error: error instanceof Error ? error.message : String(error)
124 }
125 }
126 })
127 .use(cors({
128 origin: config.domain,
129 credentials: true,
130 methods: ['GET', 'POST', 'DELETE', 'PUT', 'PATCH', 'OPTIONS'],
131 allowedHeaders: ['Content-Type', 'Authorization', 'Origin', 'X-Forwarded-Host'],
132 exposeHeaders: ['Content-Type'],
133 maxAge: 86400 // 24 hours
134 }))
135 .listen(8000)
136
137console.log(
138 `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
139)