tangled
alpha
login
or
join now
nekomimi.pet
/
wisp.place-monorepo
88
fork
atom
Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
wisp.place
88
fork
atom
overview
issues
9
pulls
pipelines
init supporter
nekomimi.pet
1 month ago
4c8ab606
abfecd6a
1/2
deploy-wisp.yml
success
35s
test.yml
failed
28s
+97
-17
9 changed files
expand all
collapse all
unified
split
apps
main-app
public
editor
editor.tsx
hooks
useDomainData.ts
useUserInfo.ts
tabs
DomainsTab.tsx
src
index.ts
lib
db.ts
routes
user.ts
packages
@wispplace
database
src
index.ts
types.ts
+10
-3
apps/main-app/public/editor/editor.tsx
···
400
400
</p>
401
401
</div>
402
402
<div className="flex items-center gap-3">
403
403
-
<span className="text-sm text-muted-foreground">
404
404
-
{userInfo?.handle || 'Loading...'}
405
405
-
</span>
403
403
+
<div className="flex items-center gap-2">
404
404
+
<span className="text-sm text-muted-foreground">
405
405
+
{userInfo?.handle || 'Loading...'}
406
406
+
</span>
407
407
+
{userInfo?.isSupporter && (
408
408
+
<Badge variant="default" className="text-xs">
409
409
+
Supporter
410
410
+
</Badge>
411
411
+
)}
412
412
+
</div>
406
413
<Button
407
414
variant="ghost"
408
415
size="sm"
+1
-1
apps/main-app/public/editor/hooks/useDomainData.ts
···
196
196
197
197
// Handle domain limit error more gracefully
198
198
if (errorMessage.includes('Domain limit reached')) {
199
199
-
alert('You have already claimed 3 wisp.place subdomains (maximum limit).')
199
199
+
alert('You have already claimed 3 wisp.place subdomains (maximum limit). Supporters get unlimited subdomains!')
200
200
await fetchDomains()
201
201
} else {
202
202
alert(`Failed to claim domain: ${errorMessage}`)
+1
apps/main-app/public/editor/hooks/useUserInfo.ts
···
3
3
export interface UserInfo {
4
4
did: string
5
5
handle: string
6
6
+
isSupporter: boolean
6
7
}
7
8
8
9
export function useUserInfo() {
+7
-3
apps/main-app/public/editor/tabs/DomainsTab.tsx
···
145
145
<CardHeader>
146
146
<CardTitle>wisp.place Subdomains</CardTitle>
147
147
<CardDescription>
148
148
-
Your free subdomains on the wisp.place network (up to 3)
148
148
+
{userInfo?.isSupporter
149
149
+
? 'Your free subdomains on the wisp.place network (unlimited as a supporter)'
150
150
+
: 'Your free subdomains on the wisp.place network (up to 3)'}
149
151
</CardDescription>
150
152
</CardHeader>
151
153
<CardContent>
···
211
213
</div>
212
214
)}
213
215
214
214
-
{wispDomains.length < 3 && (
216
216
+
{(wispDomains.length < 3 || userInfo?.isSupporter) && (
215
217
<div className="p-4 bg-muted/30 rounded-lg">
216
218
<p className="text-sm text-muted-foreground mb-4">
217
219
{wispDomains.length === 0
218
220
? 'Claim your free wisp.place subdomain'
221
221
+
: userInfo?.isSupporter
222
222
+
? `Claim another wisp.place subdomain (${wispDomains.length} claimed)`
219
223
: `Claim another wisp.place subdomain (${wispDomains.length}/3)`}
220
224
</p>
221
225
<div className="space-y-3">
···
280
284
</div>
281
285
)}
282
286
283
283
-
{wispDomains.length === 3 && (
287
287
+
{wispDomains.length === 3 && !userInfo?.isSupporter && (
284
288
<div className="p-3 bg-muted/30 rounded-lg text-center">
285
289
<p className="text-sm text-muted-foreground">
286
290
You have claimed the maximum of 3 wisp.place subdomains
+12
-1
apps/main-app/src/index.ts
···
248
248
return await Bun.file('./apps/main-app/public/editor/onboarding.html').text()
249
249
})
250
250
.get('/oauth-client-metadata.json', () => {
251
251
-
return createClientMetadata(config)
251
251
+
logger.debug('[OAuth] Client metadata requested', {
252
252
+
LOCAL_DEV: Bun.env.LOCAL_DEV,
253
253
+
DOMAIN: Bun.env.DOMAIN,
254
254
+
BASE_DOMAIN: Bun.env.BASE_DOMAIN,
255
255
+
configDomain: config.domain
256
256
+
})
257
257
+
const metadata = createClientMetadata(config)
258
258
+
logger.debug('[OAuth] Returning metadata', {
259
259
+
client_id: metadata.client_id,
260
260
+
redirect_uris: metadata.redirect_uris
261
261
+
})
262
262
+
return metadata
252
263
})
253
264
.get('/jwks.json', async ({ set }) => {
254
265
// Prevent caching to ensure clients always get fresh keys after rotation
+45
-4
apps/main-app/src/lib/db.ts
···
153
153
)
154
154
`;
155
155
156
156
+
// Supporter table - list of supporter DIDs
157
157
+
await db`
158
158
+
CREATE TABLE IF NOT EXISTS supporter (
159
159
+
did TEXT PRIMARY KEY,
160
160
+
created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW())
161
161
+
)
162
162
+
`;
163
163
+
164
164
+
// Insert initial supporter
165
165
+
await db`
166
166
+
INSERT INTO supporter (did)
167
167
+
VALUES ('did:plc:ttdrpj45ibqunmfhdsb4zdwq')
168
168
+
ON CONFLICT (did) DO NOTHING
169
169
+
`;
170
170
+
156
171
// Create indexes for common query patterns
157
172
await Promise.all([
158
173
// oauth_states cleanup queries
···
328
343
const h = handle.trim().toLowerCase();
329
344
if (!isValidHandle(h)) throw new Error('invalid_handle');
330
345
331
331
-
// Check if user already has 3 domains
332
332
-
const existingCount = await countWispDomains(did);
333
333
-
if (existingCount >= 3) {
334
334
-
throw new Error('domain_limit_reached');
346
346
+
// Check if user already has 3 domains (unless they're a supporter)
347
347
+
const supporter = await isSupporter(did);
348
348
+
if (!supporter) {
349
349
+
const existingCount = await countWispDomains(did);
350
350
+
if (existingCount >= 3) {
351
351
+
throw new Error('domain_limit_reached');
352
352
+
}
335
353
}
336
354
337
355
const domain = toDomain(h);
···
570
588
571
589
console.log('[CookieSecret] Generated new cookie signing secret');
572
590
return secret;
591
591
+
};
592
592
+
593
593
+
// Supporter management functions
594
594
+
export const isSupporter = async (did: string): Promise<boolean> => {
595
595
+
const rows = await db`SELECT 1 FROM supporter WHERE did = ${did} LIMIT 1`;
596
596
+
return rows.length > 0;
597
597
+
};
598
598
+
599
599
+
export const addSupporter = async (did: string): Promise<void> => {
600
600
+
await db`
601
601
+
INSERT INTO supporter (did)
602
602
+
VALUES (${did})
603
603
+
ON CONFLICT (did) DO NOTHING
604
604
+
`;
605
605
+
};
606
606
+
607
607
+
export const removeSupporter = async (did: string): Promise<void> => {
608
608
+
await db`DELETE FROM supporter WHERE did = ${did}`;
609
609
+
};
610
610
+
611
611
+
export const getAllSupporters = async () => {
612
612
+
const rows = await db`SELECT * FROM supporter ORDER BY created_at ASC`;
613
613
+
return rows;
573
614
};
574
615
575
616
/**
+11
-4
apps/main-app/src/routes/user.ts
···
1
1
import { Elysia, t } from 'elysia'
2
2
import { requireAuth } from '../lib/wisp-auth'
3
3
import { NodeOAuthClient } from '@atproto/oauth-client-node'
4
4
-
import { getSitesByDid, getDomainByDid, getCustomDomainsByDid, getWispDomainInfo, getDomainsBySite, getAllWispDomains } from '../lib/db'
4
4
+
import { getSitesByDid, getDomainByDid, getCustomDomainsByDid, getWispDomainInfo, getDomainsBySite, getAllWispDomains, isSupporter } from '../lib/db'
5
5
import { syncSitesFromPDS } from '../lib/sync-sites'
6
6
import { createLogger } from '@wispplace/observability'
7
7
import { getHandleForDid } from '@wispplace/atproto-utils'
···
46
46
})
47
47
/**
48
48
* GET /api/user/info
49
49
-
* Success: { did, handle }
49
49
+
* Success: { did, handle, isSupporter }
50
50
*/
51
51
.get('/info', async ({ auth }) => {
52
52
try {
···
60
60
logger.error('[User] Failed to resolve DID', err)
61
61
}
62
62
63
63
-
return {
63
63
+
// Check if user is a supporter
64
64
+
const supporter = await isSupporter(auth.did)
65
65
+
logger.debug('[User] isSupporter check', { did: auth.did, supporter })
66
66
+
67
67
+
const response = {
64
68
did: auth.did,
65
65
-
handle
69
69
+
handle,
70
70
+
isSupporter: supporter
66
71
}
72
72
+
logger.debug('[User] Returning info', response)
73
73
+
return response
67
74
} catch (err) {
68
75
logger.error('[User] Info error', err)
69
76
throw new Error('Failed to get user info')
+2
-1
packages/@wispplace/database/src/index.ts
···
20
20
CookieSecret,
21
21
AdminUser,
22
22
SiteCache,
23
23
-
SiteSettingsCache
23
23
+
SiteSettingsCache,
24
24
+
Supporter
24
25
} from './types';
+8
packages/@wispplace/database/src/types.ts
···
84
84
cached_at: number;
85
85
updated_at: number;
86
86
}
87
87
+
88
88
+
/**
89
89
+
* Supporter - list of supporter DIDs
90
90
+
*/
91
91
+
export interface Supporter {
92
92
+
did: string;
93
93
+
created_at?: number;
94
94
+
}