Monorepo for Wisp.place. A static site hosting service built on top of the AT Protocol.
1import { Hono } from 'hono'; 2import { serveStatic } from 'hono/bun'; 3import { getWispDomain, getCustomDomain, getCustomDomainByHash } from './lib/db'; 4import { resolveDid, getPdsForDid, fetchSiteRecord, downloadAndCacheSite, getCachedFilePath, isCached } from './lib/utils'; 5import { rewriteHtmlPaths, isHtmlContent } from './lib/html-rewriter'; 6import { existsSync } from 'fs'; 7 8const app = new Hono(); 9 10const BASE_HOST = process.env.BASE_HOST || 'wisp.place'; 11 12// Helper to serve files from cache 13async function serveFromCache(did: string, rkey: string, filePath: string) { 14 // Default to index.html if path is empty or ends with / 15 let requestPath = filePath || 'index.html'; 16 if (requestPath.endsWith('/')) { 17 requestPath += 'index.html'; 18 } 19 20 const cachedFile = getCachedFilePath(did, rkey, requestPath); 21 22 if (existsSync(cachedFile)) { 23 const file = Bun.file(cachedFile); 24 return new Response(file); 25 } 26 27 // Try index.html for directory-like paths 28 if (!requestPath.includes('.')) { 29 const indexFile = getCachedFilePath(did, rkey, `${requestPath}/index.html`); 30 if (existsSync(indexFile)) { 31 const file = Bun.file(indexFile); 32 return new Response(file); 33 } 34 } 35 36 return new Response('Not Found', { status: 404 }); 37} 38 39// Helper to serve files from cache with HTML path rewriting for /s/ routes 40async function serveFromCacheWithRewrite( 41 did: string, 42 rkey: string, 43 filePath: string, 44 basePath: string 45) { 46 // Default to index.html if path is empty or ends with / 47 let requestPath = filePath || 'index.html'; 48 if (requestPath.endsWith('/')) { 49 requestPath += 'index.html'; 50 } 51 52 const cachedFile = getCachedFilePath(did, rkey, requestPath); 53 54 if (existsSync(cachedFile)) { 55 const file = Bun.file(cachedFile); 56 57 // Check if this is HTML content that needs rewriting 58 if (isHtmlContent(requestPath, file.type)) { 59 const content = await file.text(); 60 const rewritten = rewriteHtmlPaths(content, basePath); 61 return new Response(rewritten, { 62 headers: { 63 'Content-Type': 'text/html; charset=utf-8', 64 }, 65 }); 66 } 67 68 // Non-HTML files served as-is 69 return new Response(file); 70 } 71 72 // Try index.html for directory-like paths 73 if (!requestPath.includes('.')) { 74 const indexFile = getCachedFilePath(did, rkey, `${requestPath}/index.html`); 75 if (existsSync(indexFile)) { 76 const file = Bun.file(indexFile); 77 const content = await file.text(); 78 const rewritten = rewriteHtmlPaths(content, basePath); 79 return new Response(rewritten, { 80 headers: { 81 'Content-Type': 'text/html; charset=utf-8', 82 }, 83 }); 84 } 85 } 86 87 return new Response('Not Found', { status: 404 }); 88} 89 90// Helper to ensure site is cached 91async function ensureSiteCached(did: string, rkey: string): Promise<boolean> { 92 if (isCached(did, rkey)) { 93 return true; 94 } 95 96 // Fetch and cache the site 97 const record = await fetchSiteRecord(did, rkey); 98 if (!record) { 99 console.error('Site record not found', did, rkey); 100 return false; 101 } 102 103 const pdsEndpoint = await getPdsForDid(did); 104 if (!pdsEndpoint) { 105 console.error('PDS not found for DID', did); 106 return false; 107 } 108 109 try { 110 await downloadAndCacheSite(did, rkey, record, pdsEndpoint); 111 return true; 112 } catch (err) { 113 console.error('Failed to cache site', did, rkey, err); 114 return false; 115 } 116} 117 118// Route 4: Direct file serving (no DB) - /s.wisp.place/:identifier/:site/* 119app.get('/s/:identifier/:site/*', async (c) => { 120 const identifier = c.req.param('identifier'); 121 const site = c.req.param('site'); 122 const filePath = c.req.path.replace(`/s/${identifier}/${site}/`, ''); 123 124 console.log('[Direct] Serving', { identifier, site, filePath }); 125 126 // Resolve identifier to DID 127 const did = await resolveDid(identifier); 128 if (!did) { 129 return c.text('Invalid identifier', 400); 130 } 131 132 // Ensure site is cached 133 const cached = await ensureSiteCached(did, site); 134 if (!cached) { 135 return c.text('Site not found', 404); 136 } 137 138 // Serve with HTML path rewriting to handle absolute paths 139 const basePath = `/s/${identifier}/${site}/`; 140 return serveFromCacheWithRewrite(did, site, filePath, basePath); 141}); 142 143// Route 3: DNS routing for custom domains - /hash.dns.wisp.place/* 144app.get('/*', async (c) => { 145 const hostname = c.req.header('host') || ''; 146 const path = c.req.path.replace(/^\//, ''); 147 148 console.log('[Request]', { hostname, path }); 149 150 // Check if this is a DNS hash subdomain 151 const dnsMatch = hostname.match(/^([a-f0-9]{16})\.dns\.(.+)$/); 152 if (dnsMatch) { 153 const hash = dnsMatch[1]; 154 const baseDomain = dnsMatch[2]; 155 156 console.log('[DNS Hash] Looking up', { hash, baseDomain }); 157 158 if (baseDomain !== BASE_HOST) { 159 return c.text('Invalid base domain', 400); 160 } 161 162 const customDomain = await getCustomDomainByHash(hash); 163 if (!customDomain) { 164 return c.text('Custom domain not found or not verified', 404); 165 } 166 167 const rkey = customDomain.rkey || 'self'; 168 const cached = await ensureSiteCached(customDomain.did, rkey); 169 if (!cached) { 170 return c.text('Site not found', 404); 171 } 172 173 return serveFromCache(customDomain.did, rkey, path); 174 } 175 176 // Route 2: Registered subdomains - /*.wisp.place/* 177 if (hostname.endsWith(`.${BASE_HOST}`)) { 178 const subdomain = hostname.replace(`.${BASE_HOST}`, ''); 179 180 console.log('[Subdomain] Looking up', { subdomain, fullDomain: hostname }); 181 182 const domainInfo = await getWispDomain(hostname); 183 if (!domainInfo) { 184 return c.text('Subdomain not registered', 404); 185 } 186 187 const rkey = domainInfo.rkey || 'self'; 188 const cached = await ensureSiteCached(domainInfo.did, rkey); 189 if (!cached) { 190 return c.text('Site not found', 404); 191 } 192 193 return serveFromCache(domainInfo.did, rkey, path); 194 } 195 196 // Route 1: Custom domains - /* 197 console.log('[Custom Domain] Looking up', { hostname }); 198 199 const customDomain = await getCustomDomain(hostname); 200 if (!customDomain) { 201 return c.text('Custom domain not found or not verified', 404); 202 } 203 204 const rkey = customDomain.rkey || 'self'; 205 const cached = await ensureSiteCached(customDomain.did, rkey); 206 if (!cached) { 207 return c.text('Site not found', 404); 208 } 209 210 return serveFromCache(customDomain.did, rkey, path); 211}); 212 213export default app;