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

able to claim 3 subdomains now

Changed files
+275 -159
hosting-service
public
src
+1 -1
hosting-service/src/lib/safe-fetch.ts
··· 25 25 const FETCH_TIMEOUT_BLOB = 120000; // 2 minutes for blob downloads 26 26 const MAX_RESPONSE_SIZE = 10 * 1024 * 1024; // 10MB 27 27 const MAX_JSON_SIZE = 1024 * 1024; // 1MB 28 - const MAX_BLOB_SIZE = 100 * 1024 * 1024; // 100MB 28 + const MAX_BLOB_SIZE = 500 * 1024 * 1024; // 500MB 29 29 const MAX_REDIRECTS = 10; 30 30 31 31 function isBlockedHost(hostname: string): boolean {
+2 -2
hosting-service/src/lib/utils.ts
··· 408 408 409 409 const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`; 410 410 411 - // Allow up to 100MB per file blob, with 2 minute timeout 412 - let content = await safeFetchBlob(blobUrl, { maxSize: 100 * 1024 * 1024, timeout: 120000 }); 411 + // Allow up to 500MB per file blob, with 5 minute timeout 412 + let content = await safeFetchBlob(blobUrl, { maxSize: 500 * 1024 * 1024, timeout: 300000 }); 413 413 414 414 console.log(`[DEBUG] ${filePath}: fetched ${content.length} bytes, base64=${base64}, encoding=${encoding}, mimeType=${mimeType}`); 415 415
+63 -44
public/editor/editor.tsx
··· 37 37 const { userInfo, loading, fetchUserInfo } = useUserInfo() 38 38 const { sites, sitesLoading, isSyncing, fetchSites, syncSites, deleteSite } = useSiteData() 39 39 const { 40 - wispDomain, 40 + wispDomains, 41 41 customDomains, 42 42 domainsLoading, 43 43 verificationStatus, ··· 46 46 verifyDomain, 47 47 deleteCustomDomain, 48 48 mapWispDomain, 49 + deleteWispDomain, 49 50 mapCustomDomain, 50 51 claimWispDomain, 51 52 checkWispAvailability ··· 74 75 if (site.domains) { 75 76 site.domains.forEach(domainInfo => { 76 77 if (domainInfo.type === 'wisp') { 77 - mappedDomains.add('wisp') 78 + // For wisp domains, use the domain itself as the identifier 79 + mappedDomains.add(`wisp:${domainInfo.domain}`) 78 80 } else if (domainInfo.id) { 79 81 mappedDomains.add(domainInfo.id) 80 82 } ··· 89 91 90 92 setIsSavingConfig(true) 91 93 try { 92 - // Determine which domains should be mapped/unmapped 93 - const shouldMapWisp = selectedDomains.has('wisp') 94 - const isCurrentlyMappedToWisp = wispDomain && wispDomain.rkey === configuringSite.rkey 94 + // Handle wisp domain mappings 95 + const selectedWispDomainIds = Array.from(selectedDomains).filter(id => id.startsWith('wisp:')) 96 + const selectedWispDomains = selectedWispDomainIds.map(id => id.replace('wisp:', '')) 97 + 98 + // Get currently mapped wisp domains 99 + const currentlyMappedWispDomains = wispDomains.filter( 100 + d => d.rkey === configuringSite.rkey 101 + ) 95 102 96 - // Handle wisp domain mapping 97 - if (shouldMapWisp && !isCurrentlyMappedToWisp) { 98 - await mapWispDomain(configuringSite.rkey) 99 - } else if (!shouldMapWisp && isCurrentlyMappedToWisp) { 100 - await mapWispDomain(null) 103 + // Unmap wisp domains that are no longer selected 104 + for (const domain of currentlyMappedWispDomains) { 105 + if (!selectedWispDomains.includes(domain.domain)) { 106 + await mapWispDomain(domain.domain, null) 107 + } 108 + } 109 + 110 + // Map newly selected wisp domains 111 + for (const domainName of selectedWispDomains) { 112 + const isAlreadyMapped = currentlyMappedWispDomains.some(d => d.domain === domainName) 113 + if (!isAlreadyMapped) { 114 + await mapWispDomain(domainName, configuringSite.rkey) 115 + } 101 116 } 102 117 103 118 // Handle custom domain mappings 104 - const selectedCustomDomainIds = Array.from(selectedDomains).filter(id => id !== 'wisp') 119 + const selectedCustomDomainIds = Array.from(selectedDomains).filter(id => !id.startsWith('wisp:')) 105 120 const currentlyMappedCustomDomains = customDomains.filter( 106 121 d => d.rkey === configuringSite.rkey 107 122 ) ··· 240 255 {/* Domains Tab */} 241 256 <TabsContent value="domains"> 242 257 <DomainsTab 243 - wispDomain={wispDomain} 258 + wispDomains={wispDomains} 244 259 customDomains={customDomains} 245 260 domainsLoading={domainsLoading} 246 261 verificationStatus={verificationStatus} ··· 248 263 onAddCustomDomain={addCustomDomain} 249 264 onVerifyDomain={verifyDomain} 250 265 onDeleteCustomDomain={deleteCustomDomain} 266 + onDeleteWispDomain={deleteWispDomain} 251 267 onClaimWispDomain={claimWispDomain} 252 268 onCheckWispAvailability={checkWispAvailability} 253 269 /> ··· 337 353 <div className="space-y-3"> 338 354 <p className="text-sm font-medium">Available Domains:</p> 339 355 340 - {wispDomain && ( 341 - <div className="flex items-center space-x-3 p-3 border rounded-lg hover:bg-muted/30"> 342 - <Checkbox 343 - id="wisp" 344 - checked={selectedDomains.has('wisp')} 345 - onCheckedChange={(checked) => { 346 - const newSelected = new Set(selectedDomains) 347 - if (checked) { 348 - newSelected.add('wisp') 349 - } else { 350 - newSelected.delete('wisp') 351 - } 352 - setSelectedDomains(newSelected) 353 - }} 354 - /> 355 - <Label 356 - htmlFor="wisp" 357 - className="flex-1 cursor-pointer" 358 - > 359 - <div className="flex items-center justify-between"> 360 - <span className="font-mono text-sm"> 361 - {wispDomain.domain} 362 - </span> 363 - <Badge variant="secondary" className="text-xs ml-2"> 364 - Wisp 365 - </Badge> 366 - </div> 367 - </Label> 368 - </div> 369 - )} 356 + {wispDomains.map((wispDomain) => { 357 + const domainId = `wisp:${wispDomain.domain}` 358 + return ( 359 + <div key={domainId} className="flex items-center space-x-3 p-3 border rounded-lg hover:bg-muted/30"> 360 + <Checkbox 361 + id={domainId} 362 + checked={selectedDomains.has(domainId)} 363 + onCheckedChange={(checked) => { 364 + const newSelected = new Set(selectedDomains) 365 + if (checked) { 366 + newSelected.add(domainId) 367 + } else { 368 + newSelected.delete(domainId) 369 + } 370 + setSelectedDomains(newSelected) 371 + }} 372 + /> 373 + <Label 374 + htmlFor={domainId} 375 + className="flex-1 cursor-pointer" 376 + > 377 + <div className="flex items-center justify-between"> 378 + <span className="font-mono text-sm"> 379 + {wispDomain.domain} 380 + </span> 381 + <Badge variant="secondary" className="text-xs ml-2"> 382 + Wisp 383 + </Badge> 384 + </div> 385 + </Label> 386 + </div> 387 + ) 388 + })} 370 389 371 390 {customDomains 372 391 .filter((d) => d.verified) ··· 407 426 </div> 408 427 ))} 409 428 410 - {customDomains.filter(d => d.verified).length === 0 && !wispDomain && ( 429 + {customDomains.filter(d => d.verified).length === 0 && wispDomains.length === 0 && ( 411 430 <p className="text-sm text-muted-foreground py-4 text-center"> 412 - No domains available. Add a custom domain or claim your wisp.place subdomain. 431 + No domains available. Add a custom domain or claim a wisp.place subdomain. 413 432 </p> 414 433 )} 415 434 </div>
+35 -8
public/editor/hooks/useDomainData.ts
··· 18 18 type VerificationStatus = 'idle' | 'verifying' | 'success' | 'error' 19 19 20 20 export function useDomainData() { 21 - const [wispDomain, setWispDomain] = useState<WispDomain | null>(null) 21 + const [wispDomains, setWispDomains] = useState<WispDomain[]>([]) 22 22 const [customDomains, setCustomDomains] = useState<CustomDomain[]>([]) 23 23 const [domainsLoading, setDomainsLoading] = useState(true) 24 24 const [verificationStatus, setVerificationStatus] = useState<{ ··· 29 29 try { 30 30 const response = await fetch('/api/user/domains') 31 31 const data = await response.json() 32 - setWispDomain(data.wispDomain) 32 + setWispDomains(data.wispDomains || []) 33 33 setCustomDomains(data.customDomains || []) 34 34 } catch (err) { 35 35 console.error('Failed to fetch domains:', err) ··· 117 117 } 118 118 } 119 119 120 - const mapWispDomain = async (siteRkey: string | null) => { 120 + const mapWispDomain = async (domain: string, siteRkey: string | null) => { 121 121 try { 122 122 const response = await fetch('/api/domain/wisp/map-site', { 123 123 method: 'POST', 124 124 headers: { 'Content-Type': 'application/json' }, 125 - body: JSON.stringify({ siteRkey }) 125 + body: JSON.stringify({ domain, siteRkey }) 126 126 }) 127 127 const data = await response.json() 128 128 if (!data.success) throw new Error('Failed to map wisp domain') ··· 133 133 } 134 134 } 135 135 136 + const deleteWispDomain = async (domain: string) => { 137 + if (!confirm('Are you sure you want to remove this wisp.place domain?')) { 138 + return false 139 + } 140 + 141 + try { 142 + const response = await fetch(`/api/domain/wisp/${encodeURIComponent(domain)}`, { 143 + method: 'DELETE' 144 + }) 145 + 146 + const data = await response.json() 147 + if (data.success) { 148 + await fetchDomains() 149 + return true 150 + } else { 151 + throw new Error('Failed to delete domain') 152 + } 153 + } catch (err) { 154 + console.error('Delete wisp domain error:', err) 155 + alert( 156 + `Failed to delete domain: ${err instanceof Error ? err.message : 'Unknown error'}` 157 + ) 158 + return false 159 + } 160 + } 161 + 136 162 const mapCustomDomain = async (domainId: string, siteRkey: string | null) => { 137 163 try { 138 164 const response = await fetch(`/api/domain/custom/${domainId}/map-site`, { ··· 168 194 console.error('Claim domain error:', err) 169 195 const errorMessage = err instanceof Error ? err.message : 'Unknown error' 170 196 171 - // Handle "Already claimed" error more gracefully 172 - if (errorMessage.includes('Already claimed')) { 173 - alert('You have already claimed a wisp.place subdomain. Please refresh the page.') 197 + // Handle domain limit error more gracefully 198 + if (errorMessage.includes('Domain limit reached')) { 199 + alert('You have already claimed 3 wisp.place subdomains (maximum limit).') 174 200 await fetchDomains() 175 201 } else { 176 202 alert(`Failed to claim domain: ${errorMessage}`) ··· 196 222 } 197 223 198 224 return { 199 - wispDomain, 225 + wispDomains, 200 226 customDomains, 201 227 domainsLoading, 202 228 verificationStatus, ··· 205 231 verifyDomain, 206 232 deleteCustomDomain, 207 233 mapWispDomain, 234 + deleteWispDomain, 208 235 mapCustomDomain, 209 236 claimWispDomain, 210 237 checkWispAvailability
+110 -85
public/editor/tabs/DomainsTab.tsx
··· 28 28 import type { UserInfo } from '../hooks/useUserInfo' 29 29 30 30 interface DomainsTabProps { 31 - wispDomain: WispDomain | null 31 + wispDomains: WispDomain[] 32 32 customDomains: CustomDomain[] 33 33 domainsLoading: boolean 34 34 verificationStatus: { [id: string]: 'idle' | 'verifying' | 'success' | 'error' } ··· 36 36 onAddCustomDomain: (domain: string) => Promise<{ success: boolean; id?: string }> 37 37 onVerifyDomain: (id: string) => Promise<void> 38 38 onDeleteCustomDomain: (id: string) => Promise<boolean> 39 + onDeleteWispDomain: (domain: string) => Promise<boolean> 39 40 onClaimWispDomain: (handle: string) => Promise<{ success: boolean; error?: string }> 40 41 onCheckWispAvailability: (handle: string) => Promise<{ available: boolean | null }> 41 42 } 42 43 43 44 export function DomainsTab({ 44 - wispDomain, 45 + wispDomains, 45 46 customDomains, 46 47 domainsLoading, 47 48 verificationStatus, ··· 49 50 onAddCustomDomain, 50 51 onVerifyDomain, 51 52 onDeleteCustomDomain, 53 + onDeleteWispDomain, 52 54 onClaimWispDomain, 53 55 onCheckWispAvailability 54 56 }: DomainsTabProps) { ··· 119 121 <div className="space-y-4 min-h-[400px]"> 120 122 <Card> 121 123 <CardHeader> 122 - <CardTitle>wisp.place Subdomain</CardTitle> 124 + <CardTitle>wisp.place Subdomains</CardTitle> 123 125 <CardDescription> 124 - Your free subdomain on the wisp.place network 126 + Your free subdomains on the wisp.place network (up to 3) 125 127 </CardDescription> 126 128 </CardHeader> 127 129 <CardContent> ··· 129 131 <div className="flex items-center justify-center py-4"> 130 132 <Loader2 className="w-6 h-6 animate-spin text-muted-foreground" /> 131 133 </div> 132 - ) : wispDomain ? ( 133 - <> 134 - <div className="flex flex-col gap-2 p-4 bg-muted/50 rounded-lg"> 135 - <div className="flex items-center gap-2"> 136 - <CheckCircle2 className="w-5 h-5 text-green-500" /> 137 - <span className="font-mono text-lg"> 138 - {wispDomain.domain} 139 - </span> 140 - </div> 141 - {wispDomain.rkey && ( 142 - <p className="text-xs text-muted-foreground ml-7"> 143 - → Mapped to site: {wispDomain.rkey} 144 - </p> 145 - )} 146 - </div> 147 - <p className="text-sm text-muted-foreground mt-3"> 148 - {wispDomain.rkey 149 - ? 'This domain is mapped to a specific site' 150 - : 'This domain is not mapped to any site yet. Configure it from the Sites tab.'} 151 - </p> 152 - </> 153 134 ) : ( 154 135 <div className="space-y-4"> 155 - <div className="p-4 bg-muted/30 rounded-lg"> 156 - <p className="text-sm text-muted-foreground mb-4"> 157 - Claim your free wisp.place subdomain 158 - </p> 159 - <div className="space-y-3"> 160 - <div className="space-y-2"> 161 - <Label htmlFor="wisp-handle">Choose your handle</Label> 162 - <div className="flex gap-2"> 163 - <div className="flex-1 relative"> 164 - <Input 165 - id="wisp-handle" 166 - placeholder="mysite" 167 - value={wispHandle} 168 - onChange={(e) => { 169 - setWispHandle(e.target.value) 170 - if (e.target.value.trim()) { 171 - checkWispAvailability(e.target.value) 172 - } else { 173 - setWispAvailability({ available: null, checking: false }) 174 - } 175 - }} 176 - disabled={isClaimingWisp} 177 - className="pr-24" 178 - /> 179 - <span className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground"> 180 - .wisp.place 181 - </span> 136 + {wispDomains.length > 0 && ( 137 + <div className="space-y-2"> 138 + {wispDomains.map((domain) => ( 139 + <div 140 + key={domain.domain} 141 + className="flex items-center justify-between p-3 border border-border rounded-lg" 142 + > 143 + <div className="flex flex-col gap-1 flex-1"> 144 + <div className="flex items-center gap-2"> 145 + <CheckCircle2 className="w-4 h-4 text-green-500" /> 146 + <span className="font-mono"> 147 + {domain.domain} 148 + </span> 149 + </div> 150 + {domain.rkey && ( 151 + <p className="text-xs text-muted-foreground ml-6"> 152 + → Mapped to site: {domain.rkey} 153 + </p> 154 + )} 155 + </div> 156 + <Button 157 + variant="ghost" 158 + size="sm" 159 + onClick={() => onDeleteWispDomain(domain.domain)} 160 + > 161 + <Trash2 className="w-4 h-4" /> 162 + </Button> 163 + </div> 164 + ))} 165 + </div> 166 + )} 167 + 168 + {wispDomains.length < 3 && ( 169 + <div className="p-4 bg-muted/30 rounded-lg"> 170 + <p className="text-sm text-muted-foreground mb-4"> 171 + {wispDomains.length === 0 172 + ? 'Claim your free wisp.place subdomain' 173 + : `Claim another wisp.place subdomain (${wispDomains.length}/3)`} 174 + </p> 175 + <div className="space-y-3"> 176 + <div className="space-y-2"> 177 + <Label htmlFor="wisp-handle">Choose your handle</Label> 178 + <div className="flex gap-2"> 179 + <div className="flex-1 relative"> 180 + <Input 181 + id="wisp-handle" 182 + placeholder="mysite" 183 + value={wispHandle} 184 + onChange={(e) => { 185 + setWispHandle(e.target.value) 186 + if (e.target.value.trim()) { 187 + checkWispAvailability(e.target.value) 188 + } else { 189 + setWispAvailability({ available: null, checking: false }) 190 + } 191 + }} 192 + disabled={isClaimingWisp} 193 + className="pr-24" 194 + /> 195 + <span className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground"> 196 + .wisp.place 197 + </span> 198 + </div> 182 199 </div> 200 + {wispAvailability.checking && ( 201 + <p className="text-xs text-muted-foreground flex items-center gap-1"> 202 + <Loader2 className="w-3 h-3 animate-spin" /> 203 + Checking availability... 204 + </p> 205 + )} 206 + {!wispAvailability.checking && wispAvailability.available === true && ( 207 + <p className="text-xs text-green-600 flex items-center gap-1"> 208 + <CheckCircle2 className="w-3 h-3" /> 209 + Available 210 + </p> 211 + )} 212 + {!wispAvailability.checking && wispAvailability.available === false && ( 213 + <p className="text-xs text-red-600 flex items-center gap-1"> 214 + <XCircle className="w-3 h-3" /> 215 + Not available 216 + </p> 217 + )} 183 218 </div> 184 - {wispAvailability.checking && ( 185 - <p className="text-xs text-muted-foreground flex items-center gap-1"> 186 - <Loader2 className="w-3 h-3 animate-spin" /> 187 - Checking availability... 188 - </p> 189 - )} 190 - {!wispAvailability.checking && wispAvailability.available === true && ( 191 - <p className="text-xs text-green-600 flex items-center gap-1"> 192 - <CheckCircle2 className="w-3 h-3" /> 193 - Available 194 - </p> 195 - )} 196 - {!wispAvailability.checking && wispAvailability.available === false && ( 197 - <p className="text-xs text-red-600 flex items-center gap-1"> 198 - <XCircle className="w-3 h-3" /> 199 - Not available 200 - </p> 201 - )} 219 + <Button 220 + onClick={handleClaimWispDomain} 221 + disabled={!wispHandle.trim() || isClaimingWisp || wispAvailability.available !== true} 222 + className="w-full" 223 + > 224 + {isClaimingWisp ? ( 225 + <> 226 + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> 227 + Claiming... 228 + </> 229 + ) : ( 230 + 'Claim Subdomain' 231 + )} 232 + </Button> 202 233 </div> 203 - <Button 204 - onClick={handleClaimWispDomain} 205 - disabled={!wispHandle.trim() || isClaimingWisp || wispAvailability.available !== true} 206 - className="w-full" 207 - > 208 - {isClaimingWisp ? ( 209 - <> 210 - <Loader2 className="w-4 h-4 mr-2 animate-spin" /> 211 - Claiming... 212 - </> 213 - ) : ( 214 - 'Claim Subdomain' 215 - )} 216 - </Button> 217 234 </div> 218 - </div> 235 + )} 236 + 237 + {wispDomains.length === 3 && ( 238 + <div className="p-3 bg-muted/30 rounded-lg text-center"> 239 + <p className="text-sm text-muted-foreground"> 240 + You have claimed the maximum of 3 wisp.place subdomains 241 + </p> 242 + </div> 243 + )} 219 244 </div> 220 245 )} 221 246 </CardContent>
+57 -12
src/routes/domain.ts
··· 10 10 isValidHandle, 11 11 toDomain, 12 12 updateDomain, 13 + countWispDomains, 14 + deleteWispDomain, 13 15 getCustomDomainInfo, 14 16 getCustomDomainById, 15 17 claimCustomDomain, ··· 84 86 try { 85 87 const { handle } = body as { handle?: string }; 86 88 const normalizedHandle = (handle || "").trim().toLowerCase(); 87 - 89 + 88 90 if (!isValidHandle(normalizedHandle)) { 89 91 throw new Error("Invalid handle"); 90 92 } 91 93 92 - // ensure user hasn't already claimed 93 - const existing = await getDomainByDid(auth.did); 94 - if (existing) { 95 - throw new Error("Already claimed"); 96 - } 97 - 94 + // Check if user already has 3 domains (handled in claimDomain) 98 95 // claim in DB 99 96 let domain: string; 100 97 try { 101 98 domain = await claimDomain(auth.did, normalizedHandle); 102 99 } catch (err) { 103 - throw new Error("Handle taken"); 100 + const message = err instanceof Error ? err.message : 'Unknown error'; 101 + if (message === 'domain_limit_reached') { 102 + throw new Error("Domain limit reached: You can only claim up to 3 wisp.place domains"); 103 + } 104 + throw new Error("Handle taken or error claiming domain"); 104 105 } 105 106 106 - // write place.wisp.domain record rkey = self 107 + // write place.wisp.domain record with unique rkey 107 108 const agent = new Agent((url, init) => auth.session.fetchHandler(url, init)); 109 + const rkey = normalizedHandle; // Use handle as rkey for uniqueness 108 110 await agent.com.atproto.repo.putRecord({ 109 111 repo: auth.did, 110 112 collection: "place.wisp.domain", 111 - rkey: "self", 113 + rkey, 112 114 record: { 113 115 $type: "place.wisp.domain", 114 116 domain, ··· 309 311 }) 310 312 .post('/wisp/map-site', async ({ body, auth }) => { 311 313 try { 312 - const { siteRkey } = body as { siteRkey: string | null }; 314 + const { domain, siteRkey } = body as { domain: string; siteRkey: string | null }; 315 + 316 + if (!domain) { 317 + throw new Error('Domain parameter required'); 318 + } 313 319 314 320 // Update wisp.place domain to point to this site 315 - await updateWispDomainSite(auth.did, siteRkey); 321 + await updateWispDomainSite(domain, siteRkey); 316 322 317 323 return { success: true }; 318 324 } catch (err) { 319 325 logger.error('[Domain] Wisp domain map error', err); 320 326 throw new Error(`Failed to map site: ${err instanceof Error ? err.message : 'Unknown error'}`); 327 + } 328 + }) 329 + .delete('/wisp/:domain', async ({ params, auth }) => { 330 + try { 331 + const { domain } = params; 332 + 333 + // Verify domain belongs to user 334 + const domainLower = domain.toLowerCase().trim(); 335 + const info = await isDomainRegistered(domainLower); 336 + 337 + if (!info.registered || info.type !== 'wisp') { 338 + throw new Error('Domain not found'); 339 + } 340 + 341 + if (info.did !== auth.did) { 342 + throw new Error('Unauthorized: You do not own this domain'); 343 + } 344 + 345 + // Delete from database 346 + await deleteWispDomain(domainLower); 347 + 348 + // Delete from PDS 349 + const agent = new Agent((url, init) => auth.session.fetchHandler(url, init)); 350 + const handle = domainLower.replace(`.${process.env.BASE_DOMAIN || 'wisp.place'}`, ''); 351 + try { 352 + await agent.com.atproto.repo.deleteRecord({ 353 + repo: auth.did, 354 + collection: "place.wisp.domain", 355 + rkey: handle, 356 + }); 357 + } catch (err) { 358 + // Record might not exist in PDS, continue anyway 359 + logger.warn('[Domain] Could not delete wisp domain from PDS', err); 360 + } 361 + 362 + return { success: true }; 363 + } catch (err) { 364 + logger.error('[Domain] Wisp domain delete error', err); 365 + throw new Error(`Failed to delete domain: ${err instanceof Error ? err.message : 'Unknown error'}`); 321 366 } 322 367 }) 323 368 .post('/custom/:id/map-site', async ({ params, body, auth }) => {
+7 -7
src/routes/user.ts
··· 2 2 import { requireAuth } from '../lib/wisp-auth' 3 3 import { NodeOAuthClient } from '@atproto/oauth-client-node' 4 4 import { Agent } from '@atproto/api' 5 - import { getSitesByDid, getDomainByDid, getCustomDomainsByDid, getWispDomainInfo, getDomainsBySite } from '../lib/db' 5 + import { getSitesByDid, getDomainByDid, getCustomDomainsByDid, getWispDomainInfo, getDomainsBySite, getAllWispDomains } from '../lib/db' 6 6 import { syncSitesFromPDS } from '../lib/sync-sites' 7 7 import { logger } from '../lib/logger' 8 8 ··· 65 65 }) 66 66 .get('/domains', async ({ auth }) => { 67 67 try { 68 - // Get wisp.place subdomain with mapping 69 - const wispDomainInfo = await getWispDomainInfo(auth.did) 68 + // Get all wisp.place subdomains with mappings (up to 3) 69 + const wispDomains = await getAllWispDomains(auth.did) 70 70 71 71 // Get custom domains 72 72 const customDomains = await getCustomDomainsByDid(auth.did) 73 73 74 74 return { 75 - wispDomain: wispDomainInfo ? { 76 - domain: wispDomainInfo.domain, 77 - rkey: wispDomainInfo.rkey || null 78 - } : null, 75 + wispDomains: wispDomains.map(d => ({ 76 + domain: d.domain, 77 + rkey: d.rkey || null 78 + })), 79 79 customDomains 80 80 } 81 81 } catch (err) {