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