+1
-1
hosting-service/src/lib/safe-fetch.ts
+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
+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
+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
+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
+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
+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
+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) {