···3434 type: 'file'
3535 /** Content blob ref */
3636 blob: BlobRef
3737+ /** Content encoding (e.g., gzip for compressed files) */
3838+ encoding?: 'gzip'
3939+ /** Original MIME type before compression */
4040+ mimeType?: string
4141+ /** True if blob content is base64-encoded (used to bypass PDS content sniffing) */
4242+ base64?: boolean
3743}
38443945const hashFile = 'file'
+98-5
hosting-service/src/lib/db.ts
···2121 verified: boolean;
2222}
23232424+// In-memory cache with TTL
2525+interface CacheEntry<T> {
2626+ data: T;
2727+ expiry: number;
2828+}
2929+3030+const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
3131+3232+class SimpleCache<T> {
3333+ private cache = new Map<string, CacheEntry<T>>();
3434+3535+ get(key: string): T | null {
3636+ const entry = this.cache.get(key);
3737+ if (!entry) return null;
3838+3939+ if (Date.now() > entry.expiry) {
4040+ this.cache.delete(key);
4141+ return null;
4242+ }
4343+4444+ return entry.data;
4545+ }
4646+4747+ set(key: string, data: T): void {
4848+ this.cache.set(key, {
4949+ data,
5050+ expiry: Date.now() + CACHE_TTL_MS,
5151+ });
5252+ }
5353+5454+ // Periodic cleanup to prevent memory leaks
5555+ cleanup(): void {
5656+ const now = Date.now();
5757+ for (const [key, entry] of this.cache.entries()) {
5858+ if (now > entry.expiry) {
5959+ this.cache.delete(key);
6060+ }
6161+ }
6262+ }
6363+}
6464+6565+// Create cache instances
6666+const wispDomainCache = new SimpleCache<DomainLookup | null>();
6767+const customDomainCache = new SimpleCache<CustomDomainLookup | null>();
6868+const customDomainHashCache = new SimpleCache<CustomDomainLookup | null>();
6969+7070+// Run cleanup every 5 minutes
7171+setInterval(() => {
7272+ wispDomainCache.cleanup();
7373+ customDomainCache.cleanup();
7474+ customDomainHashCache.cleanup();
7575+}, 5 * 60 * 1000);
7676+2477export async function getWispDomain(domain: string): Promise<DomainLookup | null> {
7878+ const key = domain.toLowerCase();
7979+8080+ // Check cache first
8181+ const cached = wispDomainCache.get(key);
8282+ if (cached !== null) {
8383+ return cached;
8484+ }
8585+8686+ // Query database
2587 const result = await sql<DomainLookup[]>`
2626- SELECT did, rkey FROM domains WHERE domain = ${domain.toLowerCase()} LIMIT 1
8888+ SELECT did, rkey FROM domains WHERE domain = ${key} LIMIT 1
2789 `;
2828- return result[0] || null;
9090+ const data = result[0] || null;
9191+9292+ // Store in cache
9393+ wispDomainCache.set(key, data);
9494+9595+ return data;
2996}
30973198export async function getCustomDomain(domain: string): Promise<CustomDomainLookup | null> {
9999+ const key = domain.toLowerCase();
100100+101101+ // Check cache first
102102+ const cached = customDomainCache.get(key);
103103+ if (cached !== null) {
104104+ return cached;
105105+ }
106106+107107+ // Query database
32108 const result = await sql<CustomDomainLookup[]>`
33109 SELECT id, domain, did, rkey, verified FROM custom_domains
3434- WHERE domain = ${domain.toLowerCase()} AND verified = true LIMIT 1
110110+ WHERE domain = ${key} AND verified = true LIMIT 1
35111 `;
3636- return result[0] || null;
112112+ const data = result[0] || null;
113113+114114+ // Store in cache
115115+ customDomainCache.set(key, data);
116116+117117+ return data;
37118}
3811939120export async function getCustomDomainByHash(hash: string): Promise<CustomDomainLookup | null> {
121121+ // Check cache first
122122+ const cached = customDomainHashCache.get(hash);
123123+ if (cached !== null) {
124124+ return cached;
125125+ }
126126+127127+ // Query database
40128 const result = await sql<CustomDomainLookup[]>`
41129 SELECT id, domain, did, rkey, verified FROM custom_domains
42130 WHERE id = ${hash} AND verified = true LIMIT 1
43131 `;
4444- return result[0] || null;
132132+ const data = result[0] || null;
133133+134134+ // Store in cache
135135+ customDomainHashCache.set(hash, data);
136136+137137+ return data;
45138}
4613947140export async function upsertSite(did: string, rkey: string, displayName?: string) {
+1-1
hosting-service/src/lib/firehose.ts
···157157 return;
158158 }
159159160160- // Cache the record with verified CID
160160+ // Cache the record with verified CID (uses atomic swap internally)
161161 await downloadAndCacheSite(did, site, fsRecord, pdsEndpoint, verifiedCid);
162162163163 // Upsert site to database
+29-10
hosting-service/src/lib/html-rewriter.ts
···1616 * Check if a path should be rewritten
1717 */
1818function shouldRewritePath(path: string): boolean {
1919- // Must start with /
2020- if (!path.startsWith('/')) return false;
1919+ // Don't rewrite empty paths
2020+ if (!path) return false;
21212222- // Don't rewrite protocol-relative URLs
2323- if (path.startsWith('//')) return false;
2222+ // Don't rewrite external URLs (http://, https://, //)
2323+ if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('//')) {
2424+ return false;
2525+ }
24262525- // Don't rewrite anchors
2626- if (path.startsWith('/#')) return false;
2727+ // Don't rewrite data URIs or other schemes (except file paths)
2828+ if (path.includes(':') && !path.startsWith('./') && !path.startsWith('../')) {
2929+ return false;
3030+ }
27312828- // Don't rewrite data URIs or other schemes
2929- if (path.includes(':')) return false;
3232+ // Don't rewrite pure anchors
3333+ if (path.startsWith('#')) return false;
30343535+ // Rewrite absolute paths (/) and relative paths (./ or ../ or plain filenames)
3136 return true;
3237}
3338···3944 return path;
4045 }
41464242- // Remove leading slash and prepend base path
4343- return basePath + path.slice(1);
4747+ // Handle absolute paths: /file.js -> /base/file.js
4848+ if (path.startsWith('/')) {
4949+ return basePath + path.slice(1);
5050+ }
5151+5252+ // Handle relative paths: ./file.js or ../file.js or file.js -> /base/file.js
5353+ // Strip leading ./ or ../ and just use the base path
5454+ let cleanPath = path;
5555+ if (cleanPath.startsWith('./')) {
5656+ cleanPath = cleanPath.slice(2);
5757+ } else if (cleanPath.startsWith('../')) {
5858+ // For sites.wisp.place, we can't go up from the site root, so just use base path
5959+ cleanPath = cleanPath.replace(/^(\.\.\/)+/, '');
6060+ }
6161+6262+ return basePath + cleanPath;
4463}
45644665/**
+75-13
hosting-service/src/lib/utils.ts
···11import { AtpAgent } from '@atproto/api';
22import type { WispFsRecord, Directory, Entry, File } from './types';
33-import { existsSync, mkdirSync, readFileSync } from 'fs';
44-import { writeFile, readFile } from 'fs/promises';
33+import { existsSync, mkdirSync, readFileSync, rmSync } from 'fs';
44+import { writeFile, readFile, rename } from 'fs/promises';
55import { safeFetchJson, safeFetchBlob } from './safe-fetch';
66import { CID } from 'multiformats/cid';
77···153153 throw new Error('Invalid record structure: root missing entries array');
154154 }
155155156156- await cacheFiles(did, rkey, record.root.entries, pdsEndpoint, '');
156156+ // Use a temporary directory with timestamp to avoid collisions
157157+ const tempSuffix = `.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
158158+ const tempDir = `${CACHE_DIR}/${did}/${rkey}${tempSuffix}`;
159159+ const finalDir = `${CACHE_DIR}/${did}/${rkey}`;
157160158158- await saveCacheMetadata(did, rkey, recordCid);
161161+ try {
162162+ // Download to temporary directory
163163+ await cacheFiles(did, rkey, record.root.entries, pdsEndpoint, '', tempSuffix);
164164+ await saveCacheMetadata(did, rkey, recordCid, tempSuffix);
165165+166166+ // Atomically replace old cache with new cache
167167+ // On POSIX systems (Linux/macOS), rename is atomic
168168+ if (existsSync(finalDir)) {
169169+ // Rename old directory to backup
170170+ const backupDir = `${finalDir}.old-${Date.now()}`;
171171+ await rename(finalDir, backupDir);
172172+173173+ try {
174174+ // Rename new directory to final location
175175+ await rename(tempDir, finalDir);
176176+177177+ // Clean up old backup
178178+ rmSync(backupDir, { recursive: true, force: true });
179179+ } catch (err) {
180180+ // If rename failed, restore backup
181181+ if (existsSync(backupDir) && !existsSync(finalDir)) {
182182+ await rename(backupDir, finalDir);
183183+ }
184184+ throw err;
185185+ }
186186+ } else {
187187+ // No existing cache, just rename temp to final
188188+ await rename(tempDir, finalDir);
189189+ }
190190+191191+ console.log('Successfully cached site atomically', did, rkey);
192192+ } catch (err) {
193193+ // Clean up temp directory on failure
194194+ if (existsSync(tempDir)) {
195195+ rmSync(tempDir, { recursive: true, force: true });
196196+ }
197197+ throw err;
198198+ }
159199}
160200161201async function cacheFiles(
···163203 site: string,
164204 entries: Entry[],
165205 pdsEndpoint: string,
166166- pathPrefix: string
206206+ pathPrefix: string,
207207+ dirSuffix: string = ''
167208): Promise<void> {
168209 for (const entry of entries) {
169210 const currentPath = pathPrefix ? `${pathPrefix}/${entry.name}` : entry.name;
170211 const node = entry.node;
171212172213 if ('type' in node && node.type === 'directory' && 'entries' in node) {
173173- await cacheFiles(did, site, node.entries, pdsEndpoint, currentPath);
214214+ await cacheFiles(did, site, node.entries, pdsEndpoint, currentPath, dirSuffix);
174215 } else if ('type' in node && node.type === 'file' && 'blob' in node) {
175175- await cacheFileBlob(did, site, currentPath, node.blob, pdsEndpoint);
216216+ const fileNode = node as File;
217217+ await cacheFileBlob(did, site, currentPath, fileNode.blob, pdsEndpoint, fileNode.encoding, fileNode.mimeType, fileNode.base64, dirSuffix);
176218 }
177219 }
178220}
···182224 site: string,
183225 filePath: string,
184226 blobRef: any,
185185- pdsEndpoint: string
227227+ pdsEndpoint: string,
228228+ encoding?: 'gzip',
229229+ mimeType?: string,
230230+ base64?: boolean,
231231+ dirSuffix: string = ''
186232): Promise<void> {
187233 const cid = extractBlobCid(blobRef);
188234 if (!cid) {
···193239 const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`;
194240195241 // Allow up to 100MB per file blob
196196- const content = await safeFetchBlob(blobUrl, { maxSize: 100 * 1024 * 1024 });
242242+ let content = await safeFetchBlob(blobUrl, { maxSize: 100 * 1024 * 1024 });
197243198198- const cacheFile = `${CACHE_DIR}/${did}/${site}/${filePath}`;
244244+ // If content is base64-encoded, decode it back to gzipped binary
245245+ if (base64 && encoding === 'gzip') {
246246+ // Convert Uint8Array to Buffer for proper string conversion
247247+ const buffer = Buffer.from(content);
248248+ const base64String = buffer.toString('utf-8');
249249+ content = Buffer.from(base64String, 'base64');
250250+ }
251251+252252+ const cacheFile = `${CACHE_DIR}/${did}/${site}${dirSuffix}/${filePath}`;
199253 const fileDir = cacheFile.substring(0, cacheFile.lastIndexOf('/'));
200254201255 if (fileDir && !existsSync(fileDir)) {
···203257 }
204258205259 await writeFile(cacheFile, content);
206206- console.log('Cached file', filePath, content.length, 'bytes');
260260+261261+ // Store metadata if file is compressed
262262+ if (encoding === 'gzip' && mimeType) {
263263+ const metaFile = `${cacheFile}.meta`;
264264+ await writeFile(metaFile, JSON.stringify({ encoding, mimeType }));
265265+ console.log('Cached file', filePath, content.length, 'bytes (gzipped,', mimeType + ')');
266266+ } else {
267267+ console.log('Cached file', filePath, content.length, 'bytes');
268268+ }
207269}
208270209271/**
···238300 return existsSync(`${CACHE_DIR}/${did}/${site}`);
239301}
240302241241-async function saveCacheMetadata(did: string, rkey: string, recordCid: string): Promise<void> {
303303+async function saveCacheMetadata(did: string, rkey: string, recordCid: string, dirSuffix: string = ''): Promise<void> {
242304 const metadata: CacheMetadata = {
243305 recordCid,
244306 cachedAt: Date.now(),
···246308 rkey
247309 };
248310249249- const metadataPath = `${CACHE_DIR}/${did}/${rkey}/.metadata.json`;
311311+ const metadataPath = `${CACHE_DIR}/${did}/${rkey}${dirSuffix}/.metadata.json`;
250312 const metadataDir = metadataPath.substring(0, metadataPath.lastIndexOf('/'));
251313252314 if (!existsSync(metadataDir)) {
+188-106
hosting-service/src/server.ts
···11-import { Hono } from 'hono';
11+import { Elysia } from 'elysia';
22+import { node } from '@elysiajs/node'
33+import { opentelemetry } from '@elysiajs/opentelemetry';
24import { getWispDomain, getCustomDomain, getCustomDomainByHash } from './lib/db';
35import { resolveDid, getPdsForDid, fetchSiteRecord, downloadAndCacheSite, getCachedFilePath, isCached, sanitizePath } from './lib/utils';
46import { rewriteHtmlPaths, isHtmlContent } from './lib/html-rewriter';
57import { existsSync, readFileSync } from 'fs';
68import { lookup } from 'mime-types';
77-88-const app = new Hono();
991010const BASE_HOST = process.env.BASE_HOST || 'wisp.place';
1111···34343535 if (existsSync(cachedFile)) {
3636 const content = readFileSync(cachedFile);
3737+ const metaFile = `${cachedFile}.meta`;
3838+3939+ // Check if file has compression metadata
4040+ if (existsSync(metaFile)) {
4141+ const meta = JSON.parse(readFileSync(metaFile, 'utf-8'));
4242+ if (meta.encoding === 'gzip' && meta.mimeType) {
4343+ // Serve gzipped content with proper headers
4444+ return new Response(content, {
4545+ headers: {
4646+ 'Content-Type': meta.mimeType,
4747+ 'Content-Encoding': 'gzip',
4848+ },
4949+ });
5050+ }
5151+ }
5252+5353+ // Serve non-compressed files normally
3754 const mimeType = lookup(cachedFile) || 'application/octet-stream';
3855 return new Response(content, {
3956 headers: {
···4764 const indexFile = getCachedFilePath(did, rkey, `${requestPath}/index.html`);
4865 if (existsSync(indexFile)) {
4966 const content = readFileSync(indexFile);
6767+ const metaFile = `${indexFile}.meta`;
6868+6969+ // Check if file has compression metadata
7070+ if (existsSync(metaFile)) {
7171+ const meta = JSON.parse(readFileSync(metaFile, 'utf-8'));
7272+ if (meta.encoding === 'gzip' && meta.mimeType) {
7373+ return new Response(content, {
7474+ headers: {
7575+ 'Content-Type': meta.mimeType,
7676+ 'Content-Encoding': 'gzip',
7777+ },
7878+ });
7979+ }
8080+ }
8181+5082 return new Response(content, {
5183 headers: {
5284 'Content-Type': 'text/html; charset=utf-8',
···74106 const cachedFile = getCachedFilePath(did, rkey, requestPath);
7510776108 if (existsSync(cachedFile)) {
7777- const mimeType = lookup(cachedFile) || 'application/octet-stream';
109109+ const metaFile = `${cachedFile}.meta`;
110110+ let mimeType = lookup(cachedFile) || 'application/octet-stream';
111111+ let isGzipped = false;
112112+113113+ // Check if file has compression metadata
114114+ if (existsSync(metaFile)) {
115115+ const meta = JSON.parse(readFileSync(metaFile, 'utf-8'));
116116+ if (meta.encoding === 'gzip' && meta.mimeType) {
117117+ mimeType = meta.mimeType;
118118+ isGzipped = true;
119119+ }
120120+ }
7812179122 // Check if this is HTML content that needs rewriting
123123+ // Note: For gzipped HTML with path rewriting, we need to decompress, rewrite, and serve uncompressed
124124+ // This is a trade-off for the sites.wisp.place domain which needs path rewriting
80125 if (isHtmlContent(requestPath, mimeType)) {
8181- const content = readFileSync(cachedFile, 'utf-8');
126126+ let content: string;
127127+ if (isGzipped) {
128128+ const { gunzipSync } = await import('zlib');
129129+ const compressed = readFileSync(cachedFile);
130130+ content = gunzipSync(compressed).toString('utf-8');
131131+ } else {
132132+ content = readFileSync(cachedFile, 'utf-8');
133133+ }
82134 const rewritten = rewriteHtmlPaths(content, basePath);
83135 return new Response(rewritten, {
84136 headers: {
···87139 });
88140 }
891419090- // Non-HTML files served with proper MIME type
142142+ // Non-HTML files: serve gzipped content as-is with proper headers
91143 const content = readFileSync(cachedFile);
144144+ if (isGzipped) {
145145+ return new Response(content, {
146146+ headers: {
147147+ 'Content-Type': mimeType,
148148+ 'Content-Encoding': 'gzip',
149149+ },
150150+ });
151151+ }
92152 return new Response(content, {
93153 headers: {
94154 'Content-Type': mimeType,
···100160 if (!requestPath.includes('.')) {
101161 const indexFile = getCachedFilePath(did, rkey, `${requestPath}/index.html`);
102162 if (existsSync(indexFile)) {
103103- const content = readFileSync(indexFile, 'utf-8');
163163+ const metaFile = `${indexFile}.meta`;
164164+ let isGzipped = false;
165165+166166+ if (existsSync(metaFile)) {
167167+ const meta = JSON.parse(readFileSync(metaFile, 'utf-8'));
168168+ if (meta.encoding === 'gzip') {
169169+ isGzipped = true;
170170+ }
171171+ }
172172+173173+ // HTML needs path rewriting, so decompress if needed
174174+ let content: string;
175175+ if (isGzipped) {
176176+ const { gunzipSync } = await import('zlib');
177177+ const compressed = readFileSync(indexFile);
178178+ content = gunzipSync(compressed).toString('utf-8');
179179+ } else {
180180+ content = readFileSync(indexFile, 'utf-8');
181181+ }
104182 const rewritten = rewriteHtmlPaths(content, basePath);
105183 return new Response(rewritten, {
106184 headers: {
···141219 }
142220}
143221144144-// Route 4: Direct file serving (no DB) - sites.wisp.place/:identifier/:site/*
145145-// This route is now handled in the catch-all route below
222222+const app = new Elysia({ adapter: node() })
223223+ .use(opentelemetry())
224224+ .get('/*', async ({ request, set }) => {
225225+ const url = new URL(request.url);
226226+ const hostname = request.headers.get('host') || '';
227227+ const rawPath = url.pathname.replace(/^\//, '');
228228+ const path = sanitizePath(rawPath);
146229147147-// Route 3: DNS routing for custom domains - /hash.dns.wisp.place/*
148148-app.get('/*', async (c) => {
149149- const hostname = c.req.header('host') || '';
150150- const rawPath = c.req.path.replace(/^\//, '');
151151- const path = sanitizePath(rawPath);
230230+ // Check if this is sites.wisp.place subdomain
231231+ if (hostname === `sites.${BASE_HOST}` || hostname === `sites.${BASE_HOST}:${process.env.PORT || 3000}`) {
232232+ // Sanitize the path FIRST to prevent path traversal
233233+ const sanitizedFullPath = sanitizePath(rawPath);
152234153153- console.log('[Request]', { hostname, path });
154154-155155- // Check if this is sites.wisp.place subdomain
156156- if (hostname === `sites.${BASE_HOST}` || hostname === `sites.${BASE_HOST}:${process.env.PORT || 3000}`) {
157157- // Sanitize the path FIRST to prevent path traversal
158158- const sanitizedFullPath = sanitizePath(rawPath);
235235+ // Extract identifier and site from sanitized path: did:plc:123abc/sitename/file.html
236236+ const pathParts = sanitizedFullPath.split('/');
237237+ if (pathParts.length < 2) {
238238+ set.status = 400;
239239+ return 'Invalid path format. Expected: /identifier/sitename/path';
240240+ }
159241160160- // Extract identifier and site from sanitized path: did:plc:123abc/sitename/file.html
161161- const pathParts = sanitizedFullPath.split('/');
162162- if (pathParts.length < 2) {
163163- return c.text('Invalid path format. Expected: /identifier/sitename/path', 400);
164164- }
242242+ const identifier = pathParts[0];
243243+ const site = pathParts[1];
244244+ const filePath = pathParts.slice(2).join('/');
165245166166- const identifier = pathParts[0];
167167- const site = pathParts[1];
168168- const filePath = pathParts.slice(2).join('/');
246246+ // Additional validation: identifier must be a valid DID or handle format
247247+ if (!identifier || identifier.length < 3 || identifier.includes('..') || identifier.includes('\0')) {
248248+ set.status = 400;
249249+ return 'Invalid identifier';
250250+ }
169251170170- console.log('[Sites] Serving', { identifier, site, filePath });
252252+ // Validate site name (rkey)
253253+ if (!isValidRkey(site)) {
254254+ set.status = 400;
255255+ return 'Invalid site name';
256256+ }
171257172172- // Additional validation: identifier must be a valid DID or handle format
173173- if (!identifier || identifier.length < 3 || identifier.includes('..') || identifier.includes('\0')) {
174174- return c.text('Invalid identifier', 400);
175175- }
258258+ // Resolve identifier to DID
259259+ const did = await resolveDid(identifier);
260260+ if (!did) {
261261+ set.status = 400;
262262+ return 'Invalid identifier';
263263+ }
176264177177- // Validate site name (rkey)
178178- if (!isValidRkey(site)) {
179179- return c.text('Invalid site name', 400);
180180- }
265265+ // Ensure site is cached
266266+ const cached = await ensureSiteCached(did, site);
267267+ if (!cached) {
268268+ set.status = 404;
269269+ return 'Site not found';
270270+ }
181271182182- // Resolve identifier to DID
183183- const did = await resolveDid(identifier);
184184- if (!did) {
185185- return c.text('Invalid identifier', 400);
272272+ // Serve with HTML path rewriting to handle absolute paths
273273+ const basePath = `/${identifier}/${site}/`;
274274+ return serveFromCacheWithRewrite(did, site, filePath, basePath);
186275 }
187276188188- // Ensure site is cached
189189- const cached = await ensureSiteCached(did, site);
190190- if (!cached) {
191191- return c.text('Site not found', 404);
192192- }
277277+ // Check if this is a DNS hash subdomain
278278+ const dnsMatch = hostname.match(/^([a-f0-9]{16})\.dns\.(.+)$/);
279279+ if (dnsMatch) {
280280+ const hash = dnsMatch[1];
281281+ const baseDomain = dnsMatch[2];
193282194194- // Serve with HTML path rewriting to handle absolute paths
195195- const basePath = `/${identifier}/${site}/`;
196196- return serveFromCacheWithRewrite(did, site, filePath, basePath);
197197- }
283283+ if (baseDomain !== BASE_HOST) {
284284+ set.status = 400;
285285+ return 'Invalid base domain';
286286+ }
198287199199- // Check if this is a DNS hash subdomain
200200- const dnsMatch = hostname.match(/^([a-f0-9]{16})\.dns\.(.+)$/);
201201- if (dnsMatch) {
202202- const hash = dnsMatch[1];
203203- const baseDomain = dnsMatch[2];
288288+ const customDomain = await getCustomDomainByHash(hash);
289289+ if (!customDomain) {
290290+ set.status = 404;
291291+ return 'Custom domain not found or not verified';
292292+ }
204293205205- console.log('[DNS Hash] Looking up', { hash, baseDomain });
294294+ const rkey = customDomain.rkey || 'self';
295295+ if (!isValidRkey(rkey)) {
296296+ set.status = 500;
297297+ return 'Invalid site configuration';
298298+ }
206299207207- if (baseDomain !== BASE_HOST) {
208208- return c.text('Invalid base domain', 400);
209209- }
300300+ const cached = await ensureSiteCached(customDomain.did, rkey);
301301+ if (!cached) {
302302+ set.status = 404;
303303+ return 'Site not found';
304304+ }
210305211211- const customDomain = await getCustomDomainByHash(hash);
212212- if (!customDomain) {
213213- return c.text('Custom domain not found or not verified', 404);
306306+ return serveFromCache(customDomain.did, rkey, path);
214307 }
215308216216- const rkey = customDomain.rkey || 'self';
217217- if (!isValidRkey(rkey)) {
218218- return c.text('Invalid site configuration', 500);
219219- }
309309+ // Route 2: Registered subdomains - /*.wisp.place/*
310310+ if (hostname.endsWith(`.${BASE_HOST}`)) {
311311+ const subdomain = hostname.replace(`.${BASE_HOST}`, '');
220312221221- const cached = await ensureSiteCached(customDomain.did, rkey);
222222- if (!cached) {
223223- return c.text('Site not found', 404);
224224- }
313313+ const domainInfo = await getWispDomain(hostname);
314314+ if (!domainInfo) {
315315+ set.status = 404;
316316+ return 'Subdomain not registered';
317317+ }
225318226226- return serveFromCache(customDomain.did, rkey, path);
227227- }
319319+ const rkey = domainInfo.rkey || 'self';
320320+ if (!isValidRkey(rkey)) {
321321+ set.status = 500;
322322+ return 'Invalid site configuration';
323323+ }
228324229229- // Route 2: Registered subdomains - /*.wisp.place/*
230230- if (hostname.endsWith(`.${BASE_HOST}`)) {
231231- const subdomain = hostname.replace(`.${BASE_HOST}`, '');
325325+ const cached = await ensureSiteCached(domainInfo.did, rkey);
326326+ if (!cached) {
327327+ set.status = 404;
328328+ return 'Site not found';
329329+ }
232330233233- console.log('[Subdomain] Looking up', { subdomain, fullDomain: hostname });
331331+ return serveFromCache(domainInfo.did, rkey, path);
332332+ }
234333235235- const domainInfo = await getWispDomain(hostname);
236236- if (!domainInfo) {
237237- return c.text('Subdomain not registered', 404);
334334+ // Route 1: Custom domains - /*
335335+ const customDomain = await getCustomDomain(hostname);
336336+ if (!customDomain) {
337337+ set.status = 404;
338338+ return 'Custom domain not found or not verified';
238339 }
239340240240- const rkey = domainInfo.rkey || 'self';
341341+ const rkey = customDomain.rkey || 'self';
241342 if (!isValidRkey(rkey)) {
242242- return c.text('Invalid site configuration', 500);
343343+ set.status = 500;
344344+ return 'Invalid site configuration';
243345 }
244346245245- const cached = await ensureSiteCached(domainInfo.did, rkey);
347347+ const cached = await ensureSiteCached(customDomain.did, rkey);
246348 if (!cached) {
247247- return c.text('Site not found', 404);
349349+ set.status = 404;
350350+ return 'Site not found';
248351 }
249352250250- return serveFromCache(domainInfo.did, rkey, path);
251251- }
252252-253253- // Route 1: Custom domains - /*
254254- console.log('[Custom Domain] Looking up', { hostname });
255255-256256- const customDomain = await getCustomDomain(hostname);
257257- if (!customDomain) {
258258- return c.text('Custom domain not found or not verified', 404);
259259- }
260260-261261- const rkey = customDomain.rkey || 'self';
262262- if (!isValidRkey(rkey)) {
263263- return c.text('Invalid site configuration', 500);
264264- }
265265-266266- const cached = await ensureSiteCached(customDomain.did, rkey);
267267- if (!cached) {
268268- return c.text('Site not found', 404);
269269- }
270270-271271- return serveFromCache(customDomain.did, rkey, path);
272272-});
353353+ return serveFromCache(customDomain.did, rkey, path);
354354+ });
273355274356export default app;