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