Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
wisp.place
1import { Elysia } from 'elysia'
2import { requireAuth } from '../lib/wisp-auth'
3import { NodeOAuthClient } from '@atproto/oauth-client-node'
4import { Agent } from '@atproto/api'
5import { deleteSite } from '../lib/db'
6import { logger } from '../lib/logger'
7import { extractSubfsUris } from '../lib/wisp-utils'
8
9export const siteRoutes = (client: NodeOAuthClient, cookieSecret: string) =>
10 new Elysia({
11 prefix: '/api/site',
12 cookie: {
13 secrets: cookieSecret,
14 sign: ['did']
15 }
16 })
17 .derive(async ({ cookie }) => {
18 const auth = await requireAuth(client, cookie)
19 return { auth }
20 })
21 .delete('/:rkey', async ({ params, auth }) => {
22 const { rkey } = params
23
24 if (!rkey) {
25 return {
26 success: false,
27 error: 'Site rkey is required'
28 }
29 }
30
31 try {
32 // Create agent with OAuth session
33 const agent = new Agent((url, init) => auth.session.fetchHandler(url, init))
34
35 // First, fetch the site record to find any subfs references
36 let subfsUris: Array<{ uri: string; path: string }> = [];
37 try {
38 const existingRecord = await agent.com.atproto.repo.getRecord({
39 repo: auth.did,
40 collection: 'place.wisp.fs',
41 rkey: rkey
42 });
43
44 if (existingRecord.data.value && typeof existingRecord.data.value === 'object' && 'root' in existingRecord.data.value) {
45 const manifest = existingRecord.data.value as any;
46 subfsUris = extractSubfsUris(manifest.root);
47
48 if (subfsUris.length > 0) {
49 console.log(`Found ${subfsUris.length} subfs records to delete`);
50 logger.info(`[Site] Found ${subfsUris.length} subfs records associated with ${rkey}`);
51 }
52 }
53 } catch (err) {
54 // Record might not exist, continue with deletion
55 console.log('Could not fetch site record for subfs cleanup, continuing...');
56 }
57
58 // Delete the main record from AT Protocol
59 try {
60 await agent.com.atproto.repo.deleteRecord({
61 repo: auth.did,
62 collection: 'place.wisp.fs',
63 rkey: rkey
64 })
65 logger.info(`[Site] Deleted site ${rkey} from PDS for ${auth.did}`)
66 } catch (err) {
67 logger.error(`[Site] Failed to delete site ${rkey} from PDS`, err)
68 throw new Error('Failed to delete site from AT Protocol')
69 }
70
71 // Delete associated subfs records
72 if (subfsUris.length > 0) {
73 console.log(`Deleting ${subfsUris.length} associated subfs records...`);
74
75 await Promise.all(
76 subfsUris.map(async ({ uri }) => {
77 try {
78 // Parse URI: at://did/collection/rkey
79 const parts = uri.replace('at://', '').split('/');
80 const subRkey = parts[2];
81
82 await agent.com.atproto.repo.deleteRecord({
83 repo: auth.did,
84 collection: 'place.wisp.subfs',
85 rkey: subRkey
86 });
87
88 console.log(` 🗑️ Deleted subfs: ${uri}`);
89 logger.info(`[Site] Deleted subfs record: ${uri}`);
90 } catch (err: any) {
91 // Log but don't fail if subfs deletion fails
92 console.warn(`Failed to delete subfs ${uri}:`, err?.message);
93 logger.warn(`[Site] Failed to delete subfs ${uri}`, err);
94 }
95 })
96 );
97
98 logger.info(`[Site] Deleted ${subfsUris.length} subfs records for ${rkey}`);
99 }
100
101 // Delete from database
102 const result = await deleteSite(auth.did, rkey)
103 if (!result.success) {
104 throw new Error('Failed to delete site from database')
105 }
106
107 logger.info(`[Site] Successfully deleted site ${rkey} for ${auth.did}`)
108
109 return {
110 success: true,
111 message: 'Site deleted successfully'
112 }
113 } catch (err) {
114 logger.error('[Site] Delete error', err)
115 return {
116 success: false,
117 error: err instanceof Error ? err.message : 'Failed to delete site'
118 }
119 }
120 })