Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
wisp.place
1import { Elysia, t } from 'elysia'
2import { requireAuth } from '../lib/wisp-auth'
3import { NodeOAuthClient } from '@atproto/oauth-client-node'
4import { getSitesByDid, getDomainByDid, getCustomDomainsByDid, getWispDomainInfo, getDomainsBySite, getAllWispDomains } from '../lib/db'
5import { syncSitesFromPDS } from '../lib/sync-sites'
6import { createLogger } from '@wisp/observability'
7import { createDidResolver, extractAtprotoData } from '@atproto-labs/did-resolver'
8
9const logger = createLogger('main-app')
10const didResolver = createDidResolver({})
11
12export const userRoutes = (client: NodeOAuthClient, cookieSecret: string) =>
13 new Elysia({
14 prefix: '/api/user',
15 cookie: {
16 secrets: cookieSecret,
17 sign: ['did']
18 }
19 })
20 .derive(async ({ cookie }) => {
21 const auth = await requireAuth(client, cookie)
22 return { auth }
23 })
24 .get('/status', async ({ auth }) => {
25 try {
26 // Check if user has any sites
27 const sites = await getSitesByDid(auth.did)
28
29 // Check if user has claimed a domain
30 const domain = await getDomainByDid(auth.did)
31
32 return {
33 did: auth.did,
34 hasSites: sites.length > 0,
35 hasDomain: !!domain,
36 domain: domain || null,
37 sitesCount: sites.length
38 }
39 } catch (err) {
40 logger.error('[User] Status error', err)
41 throw new Error('Failed to get user status')
42 }
43 })
44 .get('/info', async ({ auth }) => {
45 try {
46 let handle = 'unknown'
47 try {
48 const didDoc = await didResolver.resolve(auth.did)
49 const atprotoData = extractAtprotoData(didDoc)
50
51 if (atprotoData.aka) {
52 handle = atprotoData.aka
53 }
54 } catch (err) {
55
56 logger.error('[User] Failed to resolve DID', err)
57 }
58
59 return {
60 did: auth.did,
61 handle
62 }
63 } catch (err) {
64 logger.error('[User] Info error', err)
65 throw new Error('Failed to get user info')
66 }
67 })
68 .get('/sites', async ({ auth }) => {
69 try {
70 const sites = await getSitesByDid(auth.did)
71 return { sites }
72 } catch (err) {
73 logger.error('[User] Sites error', err)
74 throw new Error('Failed to get sites')
75 }
76 })
77 .get('/domains', async ({ auth }) => {
78 try {
79 // Get all wisp.place subdomains with mappings (up to 3)
80 const wispDomains = await getAllWispDomains(auth.did)
81
82 // Get custom domains
83 const customDomains = await getCustomDomainsByDid(auth.did)
84
85 return {
86 wispDomains: wispDomains.map(d => ({
87 domain: d.domain,
88 rkey: d.rkey || null
89 })),
90 customDomains
91 }
92 } catch (err) {
93 logger.error('[User] Domains error', err)
94 throw new Error('Failed to get domains')
95 }
96 })
97 .post('/sync', async ({ auth }) => {
98 try {
99 logger.debug('[User] Manual sync requested for', { did: auth.did })
100 const result = await syncSitesFromPDS(auth.did, auth.session)
101
102 return {
103 success: true,
104 synced: result.synced,
105 errors: result.errors
106 }
107 } catch (err) {
108 logger.error('[User] Sync error', err)
109 throw new Error('Failed to sync sites')
110 }
111 })
112 .get('/site/:rkey/domains', async ({ auth, params }) => {
113 try {
114 const { rkey } = params
115 const domains = await getDomainsBySite(auth.did, rkey)
116
117 return {
118 rkey,
119 domains
120 }
121 } catch (err) {
122 logger.error('[User] Site domains error', err)
123 throw new Error('Failed to get domains for site')
124 }
125 })