Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

fix jsonb double-encoding, s3 error handling, base host routing

+57 -20
+8 -8
apps/firehose-service/src/lib/db.ts
··· 54 54 recordCid: string, 55 55 fileCids: Record<string, string> 56 56 ): Promise<void> { 57 - const fileCidsJson = JSON.stringify(fileCids ?? {}); 58 57 console.log(`[DB] upsertSiteCache starting for ${did}/${rkey}`); 59 58 try { 60 59 await sql` 61 60 INSERT INTO site_cache (did, rkey, record_cid, file_cids, cached_at, updated_at) 62 - VALUES (${did}, ${rkey}, ${recordCid}, ${fileCidsJson}::jsonb, EXTRACT(EPOCH FROM NOW()), EXTRACT(EPOCH FROM NOW())) 61 + VALUES (${did}, ${rkey}, ${recordCid}, ${sql.json(fileCids ?? {})}, EXTRACT(EPOCH FROM NOW()), EXTRACT(EPOCH FROM NOW())) 63 62 ON CONFLICT (did, rkey) 64 63 DO UPDATE SET 65 64 record_cid = EXCLUDED.record_cid, ··· 94 93 const directoryListing = settings.directoryListing ?? false; 95 94 const spaMode = settings.spaMode ?? null; 96 95 const custom404 = settings.custom404 ?? null; 97 - const indexFilesJson = JSON.stringify(settings.indexFiles ?? []); 98 96 const cleanUrls = settings.cleanUrls ?? true; 99 - const headersJson = JSON.stringify(settings.headers ?? []); 97 + 98 + const indexFiles = settings.indexFiles ?? []; 99 + const headers = settings.headers ?? []; 100 100 101 101 console.log(`[DB] upsertSiteSettingsCache starting for ${did}/${rkey}`, { 102 102 directoryListing, 103 103 spaMode, 104 104 custom404, 105 - indexFiles: indexFilesJson, 105 + indexFiles, 106 106 cleanUrls, 107 - headers: headersJson, 107 + headers, 108 108 }); 109 109 110 110 try { ··· 117 117 ${directoryListing}, 118 118 ${spaMode}, 119 119 ${custom404}, 120 - ${indexFilesJson}::jsonb, 120 + ${sql.json(indexFiles)}, 121 121 ${cleanUrls}, 122 - ${headersJson}::jsonb, 122 + ${sql.json(headers)}, 123 123 EXTRACT(EPOCH FROM NOW()), 124 124 EXTRACT(EPOCH FROM NOW()) 125 125 )
+2 -1
apps/hosting-service/.env.example
··· 3 3 4 4 # Server 5 5 PORT=3001 6 - BASE_HOST=wisp.place 6 + # Base domain (e.g., "localhost" for sites.localhost, "wisp.place" for sites.wisp.place) 7 + BASE_HOST=localhost 7 8 8 9 # Redis (cache invalidation + revalidation queue) 9 10 REDIS_URL=redis://localhost:6379
+1 -2
apps/hosting-service/src/lib/db.ts
··· 128 128 recordCid: string, 129 129 fileCids: Record<string, string> 130 130 ): Promise<void> { 131 - const fileCidsJson = JSON.stringify(fileCids ?? {}); 132 131 try { 133 132 await sql` 134 133 INSERT INTO site_cache (did, rkey, record_cid, file_cids, cached_at, updated_at) 135 - VALUES (${did}, ${rkey}, ${recordCid}, ${fileCidsJson}::jsonb, EXTRACT(EPOCH FROM NOW()), EXTRACT(EPOCH FROM NOW())) 134 + VALUES (${did}, ${rkey}, ${recordCid}, ${sql.json(fileCids ?? {})}, EXTRACT(EPOCH FROM NOW()), EXTRACT(EPOCH FROM NOW())) 136 135 ON CONFLICT (did, rkey) 137 136 DO UPDATE SET 138 137 record_cid = EXCLUDED.record_cid,
+6 -2
apps/hosting-service/src/lib/file-serving.ts
··· 158 158 */ 159 159 async function ensureSiteCached(did: string, rkey: string): Promise<void> { 160 160 const existing = await getSiteCache(did, rkey); 161 - if (existing) return; // Site is known, proceed normally 161 + if (existing) { 162 + console.log(`[FileServing] Site ${did}/${rkey} found in DB, proceeding normally`); 163 + return; 164 + } 162 165 163 166 // Site is completely unknown — try on-demand fetch 164 167 console.log(`[FileServing] Site ${did}/${rkey} not in DB, attempting on-demand cache`); 165 - await fetchAndCacheSite(did, rkey); 168 + const success = await fetchAndCacheSite(did, rkey); 169 + console.log(`[FileServing] On-demand cache for ${did}/${rkey}: ${success ? 'success' : 'failed'}`); 166 170 } 167 171 168 172 /**
+36 -6
apps/hosting-service/src/lib/storage.ts
··· 59 59 60 60 constructor(private tier: StorageTier) {} 61 61 62 - // Read operations - pass through to underlying tier 62 + // Read operations - pass through to underlying tier, catch errors as cache misses 63 63 async get(key: string) { 64 - return this.tier.get(key); 64 + try { 65 + return await this.tier.get(key); 66 + } catch (err) { 67 + this.logReadError('get', key, err); 68 + return null; 69 + } 65 70 } 66 71 67 72 async getWithMetadata(key: string) { 68 - return this.tier.getWithMetadata?.(key) ?? null; 73 + try { 74 + return await this.tier.getWithMetadata?.(key) ?? null; 75 + } catch (err) { 76 + this.logReadError('getWithMetadata', key, err); 77 + return null; 78 + } 69 79 } 70 80 71 81 async getStream(key: string) { 72 - return this.tier.getStream?.(key) ?? null; 82 + try { 83 + return await this.tier.getStream?.(key) ?? null; 84 + } catch (err) { 85 + this.logReadError('getStream', key, err); 86 + return null; 87 + } 73 88 } 74 89 75 90 async exists(key: string) { 76 - return this.tier.exists(key); 91 + try { 92 + return await this.tier.exists(key); 93 + } catch (err) { 94 + this.logReadError('exists', key, err); 95 + return false; 96 + } 77 97 } 78 98 79 99 async getMetadata(key: string) { 80 - return this.tier.getMetadata(key); 100 + try { 101 + return await this.tier.getMetadata(key); 102 + } catch (err) { 103 + this.logReadError('getMetadata', key, err); 104 + return null; 105 + } 81 106 } 82 107 83 108 async *listKeys(prefix?: string) { ··· 111 136 112 137 async clear() { 113 138 this.logWriteSkip('clear', 'all keys'); 139 + } 140 + 141 + private logReadError(operation: string, key: string, err: unknown) { 142 + const msg = err instanceof Error ? err.message : String(err); 143 + console.warn(`[Storage] S3 read error (${operation}) for ${key}: ${msg}`); 114 144 } 115 145 116 146 private logWriteSkip(operation: string, key: string) {
+4 -1
apps/hosting-service/src/server.ts
··· 14 14 import { serveFromCache, serveFromCacheWithRewrite } from './lib/file-serving'; 15 15 import { getRevalidateMetrics } from './lib/revalidate-metrics'; 16 16 17 - const BASE_HOST = process.env.BASE_HOST || 'wisp.place'; 17 + const BASE_HOST_ENV = process.env.BASE_HOST || 'wisp.place'; 18 + const BASE_HOST = BASE_HOST_ENV.split(':')[0] || BASE_HOST_ENV; 18 19 19 20 const app = new Hono(); 20 21 ··· 41 42 const hostnameWithoutPort = hostname.split(':')[0] || ''; 42 43 const rawPath = url.pathname.replace(/^\//, ''); 43 44 const path = sanitizePath(rawPath); 45 + 46 + console.log(`[Server] Request: host=${hostname} hostnameWithoutPort=${hostnameWithoutPort} path=${path} BASE_HOST=${BASE_HOST}`); 44 47 45 48 // Check if this is sites.wisp.place subdomain (strip port for comparison) 46 49 if (hostnameWithoutPort === `sites.${BASE_HOST}`) {