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'
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)