+3
-2
bun.lock
+3
-2
bun.lock
···
1
{
2
"lockfileVersion": 1,
3
"workspaces": {
4
"": {
5
"name": "elysia-static",
···
21
"@radix-ui/react-tabs": "^1.1.13",
22
"@tanstack/react-query": "^5.90.2",
23
"actor-typeahead": "^0.1.1",
24
-
"atproto-ui": "^0.11.1",
25
"class-variance-authority": "^0.7.1",
26
"clsx": "^2.1.1",
27
"elysia": "latest",
···
401
402
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
403
404
-
"atproto-ui": ["atproto-ui@0.11.1", "", { "dependencies": { "@atcute/atproto": "^3.1.7", "@atcute/bluesky": "^3.2.3", "@atcute/client": "^4.0.3", "@atcute/identity-resolver": "^1.1.3", "@atcute/tangled": "^1.0.10" }, "peerDependencies": { "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0" }, "optionalPeers": ["react-dom"] }, "sha512-RpX9OGx3GDw0uL2X0Lw0bgzqEKKhfMeFuTUIgJuAa3W3MlLBH6h4qOWzaHXdrVQpru+6SQ0HznfRlQHK6nYRkQ=="],
405
406
"await-lock": ["await-lock@2.2.2", "", {}, "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="],
407
···
1
{
2
"lockfileVersion": 1,
3
+
"configVersion": 0,
4
"workspaces": {
5
"": {
6
"name": "elysia-static",
···
22
"@radix-ui/react-tabs": "^1.1.13",
23
"@tanstack/react-query": "^5.90.2",
24
"actor-typeahead": "^0.1.1",
25
+
"atproto-ui": "^0.11.3",
26
"class-variance-authority": "^0.7.1",
27
"clsx": "^2.1.1",
28
"elysia": "latest",
···
402
403
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
404
405
+
"atproto-ui": ["atproto-ui@0.11.3", "", { "dependencies": { "@atcute/atproto": "^3.1.7", "@atcute/bluesky": "^3.2.3", "@atcute/client": "^4.0.3", "@atcute/identity-resolver": "^1.1.3", "@atcute/tangled": "^1.0.10" }, "peerDependencies": { "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0" }, "optionalPeers": ["react-dom"] }, "sha512-NIBsORuo9lpCpr1SNKcKhNvqOVpsEy9IoHqFe1CM9gNTArpQL1hUcoP1Cou9a1O5qzCul9kaiu5xBHnB81I/WQ=="],
406
407
"await-lock": ["await-lock@2.2.2", "", {}, "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="],
408
+1
-1
package.json
+1
-1
package.json
+3
-4
public/index.tsx
+3
-4
public/index.tsx
···
13
import { Button } from '@public/components/ui/button'
14
import { Card } from '@public/components/ui/card'
15
import { BlueskyPostList, BlueskyProfile, BlueskyPost, AtProtoProvider, useLatestRecord, type AtProtoStyles, type FeedPostRecord } from 'atproto-ui'
16
-
import 'atproto-ui/styles.css'
17
18
//Credit to https://tangled.org/@jakelazaroff.com/actor-typeahead
19
interface Actor {
···
212
width: '100%',
213
listStyle: 'none',
214
overflow: 'hidden',
215
-
backgroundColor: 'rgba(255, 255, 255, 0.7)',
216
backgroundClip: 'padding-box',
217
backdropFilter: 'blur(12px)',
218
WebkitBackdropFilter: 'blur(12px)',
219
-
border: '1px solid hsl(var(--border))',
220
borderRadius: '8px',
221
boxShadow: '0 6px 6px -4px rgba(0, 0, 0, 0.2)',
222
padding: '4px',
···
278
whiteSpace: 'nowrap',
279
overflow: 'hidden',
280
textOverflow: 'ellipsis',
281
-
color: 'hsl(var(--foreground))'
282
}}
283
>
284
{actor.handle}
···
13
import { Button } from '@public/components/ui/button'
14
import { Card } from '@public/components/ui/card'
15
import { BlueskyPostList, BlueskyProfile, BlueskyPost, AtProtoProvider, useLatestRecord, type AtProtoStyles, type FeedPostRecord } from 'atproto-ui'
16
17
//Credit to https://tangled.org/@jakelazaroff.com/actor-typeahead
18
interface Actor {
···
211
width: '100%',
212
listStyle: 'none',
213
overflow: 'hidden',
214
+
backgroundColor: 'rgba(255, 255, 255, 0.8)',
215
backgroundClip: 'padding-box',
216
backdropFilter: 'blur(12px)',
217
WebkitBackdropFilter: 'blur(12px)',
218
+
border: '1px solid rgba(0, 0, 0, 0.1)',
219
borderRadius: '8px',
220
boxShadow: '0 6px 6px -4px rgba(0, 0, 0, 0.2)',
221
padding: '4px',
···
277
whiteSpace: 'nowrap',
278
overflow: 'hidden',
279
textOverflow: 'ellipsis',
280
+
color: '#000000'
281
}}
282
>
283
{actor.handle}
+10
-2
src/index.ts
+10
-2
src/index.ts
···
12
cleanupExpiredSessions,
13
rotateKeysIfNeeded
14
} from './lib/oauth-client'
15
import { authRoutes } from './routes/auth'
16
import { wispRoutes } from './routes/wisp'
17
import { domainRoutes } from './routes/domain'
···
30
31
// Initialize admin setup (prompt if no admin exists)
32
await promptAdminSetup()
33
34
const client = await getOAuthClient(config)
35
···
63
maxRequestBodySize: 1024 * 1024 * 128 * 3,
64
development: Bun.env.NODE_ENV !== 'production' ? true : false,
65
id: Bun.env.NODE_ENV !== 'production' ? undefined : null,
66
}
67
})
68
// Observability middleware
···
96
})
97
.onError(observabilityMiddleware('main-app').onError)
98
.use(csrfProtection())
99
-
.use(authRoutes(client))
100
.use(wispRoutes(client))
101
.use(domainRoutes(client))
102
.use(userRoutes(client))
103
.use(siteRoutes(client))
104
-
.use(adminRoutes())
105
.use(
106
await staticPlugin({
107
prefix: '/'
···
12
cleanupExpiredSessions,
13
rotateKeysIfNeeded
14
} from './lib/oauth-client'
15
+
import { getCookieSecret } from './lib/db'
16
import { authRoutes } from './routes/auth'
17
import { wispRoutes } from './routes/wisp'
18
import { domainRoutes } from './routes/domain'
···
31
32
// Initialize admin setup (prompt if no admin exists)
33
await promptAdminSetup()
34
+
35
+
// Get or generate cookie signing secret
36
+
const cookieSecret = await getCookieSecret()
37
38
const client = await getOAuthClient(config)
39
···
67
maxRequestBodySize: 1024 * 1024 * 128 * 3,
68
development: Bun.env.NODE_ENV !== 'production' ? true : false,
69
id: Bun.env.NODE_ENV !== 'production' ? undefined : null,
70
+
},
71
+
cookie: {
72
+
secrets: cookieSecret,
73
+
sign: true
74
}
75
})
76
// Observability middleware
···
104
})
105
.onError(observabilityMiddleware('main-app').onError)
106
.use(csrfProtection())
107
+
.use(authRoutes(client, cookieSecret))
108
.use(wispRoutes(client))
109
.use(domainRoutes(client))
110
.use(userRoutes(client))
111
.use(siteRoutes(client))
112
+
.use(adminRoutes(cookieSecret))
113
.use(
114
await staticPlugin({
115
prefix: '/'
+29
src/lib/db.ts
+29
src/lib/db.ts
···
36
)
37
`;
38
39
+
// Cookie secrets table for signed cookies
40
+
await db`
41
+
CREATE TABLE IF NOT EXISTS cookie_secrets (
42
+
id TEXT PRIMARY KEY DEFAULT 'default',
43
+
secret TEXT NOT NULL,
44
+
created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW())
45
+
)
46
+
`;
47
+
48
// Domains table maps subdomain -> DID (now supports up to 3 domains per user)
49
await db`
50
CREATE TABLE IF NOT EXISTS domains (
···
725
total: Number(wispCount[0]?.count || 0) + Number(customCount[0]?.count || 0),
726
};
727
};
728
+
729
+
// Cookie secret management - ensure we have a secret for signing cookies
730
+
export const getCookieSecret = async (): Promise<string> => {
731
+
// Check if secret already exists
732
+
const rows = await db`SELECT secret FROM cookie_secrets WHERE id = 'default' LIMIT 1`;
733
+
734
+
if (rows.length > 0) {
735
+
return rows[0].secret as string;
736
+
}
737
+
738
+
// Generate new secret if none exists
739
+
const secret = crypto.randomUUID() + crypto.randomUUID(); // 72 character random string
740
+
await db`
741
+
INSERT INTO cookie_secrets (id, secret, created_at)
742
+
VALUES ('default', ${secret}, EXTRACT(EPOCH FROM NOW()))
743
+
`;
744
+
745
+
console.log('[CookieSecret] Generated new cookie signing secret');
746
+
return secret;
747
+
};
+65
-3
src/routes/admin.ts
+65
-3
src/routes/admin.ts
···
4
import { logCollector, errorTracker, metricsCollector } from '../lib/observability'
5
import { db } from '../lib/db'
6
7
-
export const adminRoutes = () =>
8
new Elysia({ prefix: '/api/admin' })
9
// Login
10
.post(
···
35
body: t.Object({
36
username: t.String(),
37
password: t.String()
38
})
39
}
40
)
···
47
}
48
cookie.admin_session.remove()
49
return { success: true }
50
})
51
52
// Check auth status
···
65
authenticated: true,
66
username: session.username
67
}
68
})
69
70
// Get logs (protected)
···
109
)
110
111
return { logs: allLogs.slice(0, filter.limit || 100) }
112
})
113
114
// Get errors (protected)
···
147
)
148
149
return { errors: allErrors.slice(0, filter.limit || 100) }
150
})
151
152
// Get metrics (protected)
···
189
hostingService: hostingServiceStats,
190
timeWindow
191
}
192
})
193
194
// Get database stats (protected)
···
204
205
// Get recent sites (including those without domains)
206
const recentSites = await db`
207
-
SELECT
208
s.did,
209
s.rkey,
210
s.display_name,
···
235
message: error instanceof Error ? error.message : String(error)
236
}
237
}
238
})
239
240
// Get sites listing (protected)
···
247
248
try {
249
const sites = await db`
250
-
SELECT
251
s.did,
252
s.rkey,
253
s.display_name,
···
282
message: error instanceof Error ? error.message : String(error)
283
}
284
}
285
})
286
287
// Get system health (protected)
···
301
},
302
timestamp: new Date().toISOString()
303
}
304
})
305
···
4
import { logCollector, errorTracker, metricsCollector } from '../lib/observability'
5
import { db } from '../lib/db'
6
7
+
export const adminRoutes = (cookieSecret: string) =>
8
new Elysia({ prefix: '/api/admin' })
9
// Login
10
.post(
···
35
body: t.Object({
36
username: t.String(),
37
password: t.String()
38
+
}),
39
+
cookie: t.Cookie({
40
+
admin_session: t.String()
41
+
}, {
42
+
secrets: cookieSecret,
43
+
sign: ['admin_session']
44
})
45
}
46
)
···
53
}
54
cookie.admin_session.remove()
55
return { success: true }
56
+
}, {
57
+
cookie: t.Cookie({
58
+
admin_session: t.Optional(t.String())
59
+
}, {
60
+
secrets: cookieSecret,
61
+
sign: ['admin_session']
62
+
})
63
})
64
65
// Check auth status
···
78
authenticated: true,
79
username: session.username
80
}
81
+
}, {
82
+
cookie: t.Cookie({
83
+
admin_session: t.Optional(t.String())
84
+
}, {
85
+
secrets: cookieSecret,
86
+
sign: ['admin_session']
87
+
})
88
})
89
90
// Get logs (protected)
···
129
)
130
131
return { logs: allLogs.slice(0, filter.limit || 100) }
132
+
}, {
133
+
cookie: t.Cookie({
134
+
admin_session: t.Optional(t.String())
135
+
}, {
136
+
secrets: cookieSecret,
137
+
sign: ['admin_session']
138
+
})
139
})
140
141
// Get errors (protected)
···
174
)
175
176
return { errors: allErrors.slice(0, filter.limit || 100) }
177
+
}, {
178
+
cookie: t.Cookie({
179
+
admin_session: t.Optional(t.String())
180
+
}, {
181
+
secrets: cookieSecret,
182
+
sign: ['admin_session']
183
+
})
184
})
185
186
// Get metrics (protected)
···
223
hostingService: hostingServiceStats,
224
timeWindow
225
}
226
+
}, {
227
+
cookie: t.Cookie({
228
+
admin_session: t.Optional(t.String())
229
+
}, {
230
+
secrets: cookieSecret,
231
+
sign: ['admin_session']
232
+
})
233
})
234
235
// Get database stats (protected)
···
245
246
// Get recent sites (including those without domains)
247
const recentSites = await db`
248
+
SELECT
249
s.did,
250
s.rkey,
251
s.display_name,
···
276
message: error instanceof Error ? error.message : String(error)
277
}
278
}
279
+
}, {
280
+
cookie: t.Cookie({
281
+
admin_session: t.Optional(t.String())
282
+
}, {
283
+
secrets: cookieSecret,
284
+
sign: ['admin_session']
285
+
})
286
})
287
288
// Get sites listing (protected)
···
295
296
try {
297
const sites = await db`
298
+
SELECT
299
s.did,
300
s.rkey,
301
s.display_name,
···
330
message: error instanceof Error ? error.message : String(error)
331
}
332
}
333
+
}, {
334
+
cookie: t.Cookie({
335
+
admin_session: t.Optional(t.String())
336
+
}, {
337
+
secrets: cookieSecret,
338
+
sign: ['admin_session']
339
+
})
340
})
341
342
// Get system health (protected)
···
356
},
357
timestamp: new Date().toISOString()
358
}
359
+
}, {
360
+
cookie: t.Cookie({
361
+
admin_session: t.Optional(t.String())
362
+
}, {
363
+
secrets: cookieSecret,
364
+
sign: ['admin_session']
365
+
})
366
})
367
+32
-6
src/routes/auth.ts
+32
-6
src/routes/auth.ts
···
1
-
import { Elysia } from 'elysia'
2
import { NodeOAuthClient } from '@atproto/oauth-client-node'
3
-
import { getSitesByDid, getDomainByDid } from '../lib/db'
4
import { syncSitesFromPDS } from '../lib/sync-sites'
5
import { authenticateRequest } from '../lib/wisp-auth'
6
import { logger } from '../lib/observability'
7
8
-
export const authRoutes = (client: NodeOAuthClient) => new Elysia()
9
.post('/api/auth/signin', async (c) => {
10
let handle = 'unknown'
11
try {
···
36
}
37
38
const cookieSession = c.cookie
39
-
cookieSession.did.value = session.did
40
41
// Sync sites from PDS to database cache
42
logger.debug('[Auth] Syncing sites from PDS for', session.did)
···
66
logger.error('[Auth] OAuth callback error', err)
67
return c.redirect('/?error=auth_failed')
68
}
69
})
70
.post('/api/auth/logout', async (c) => {
71
try {
···
73
const did = cookieSession.did?.value
74
75
// Clear the session cookie
76
-
cookieSession.did.value = ''
77
-
cookieSession.did.maxAge = 0
78
79
// If we have a DID, try to revoke the OAuth session
80
if (did && typeof did === 'string') {
···
92
logger.error('[Auth] Logout error', err)
93
return { error: 'Logout failed' }
94
}
95
})
96
.get('/api/auth/status', async (c) => {
97
try {
···
109
logger.error('[Auth] Status check error', err)
110
return { authenticated: false }
111
}
112
})
···
1
+
import { Elysia, t } from 'elysia'
2
import { NodeOAuthClient } from '@atproto/oauth-client-node'
3
+
import { getSitesByDid, getDomainByDid, getCookieSecret } from '../lib/db'
4
import { syncSitesFromPDS } from '../lib/sync-sites'
5
import { authenticateRequest } from '../lib/wisp-auth'
6
import { logger } from '../lib/observability'
7
8
+
export const authRoutes = (client: NodeOAuthClient, cookieSecret: string) => new Elysia()
9
.post('/api/auth/signin', async (c) => {
10
let handle = 'unknown'
11
try {
···
36
}
37
38
const cookieSession = c.cookie
39
+
cookieSession.did.set({
40
+
value: session.did,
41
+
httpOnly: true,
42
+
secure: process.env.NODE_ENV === 'production',
43
+
sameSite: 'lax',
44
+
maxAge: 30 * 24 * 60 * 60 // 30 days
45
+
})
46
47
// Sync sites from PDS to database cache
48
logger.debug('[Auth] Syncing sites from PDS for', session.did)
···
72
logger.error('[Auth] OAuth callback error', err)
73
return c.redirect('/?error=auth_failed')
74
}
75
+
}, {
76
+
cookie: t.Cookie({
77
+
did: t.Optional(t.String())
78
+
}, {
79
+
secrets: cookieSecret,
80
+
sign: ['did']
81
+
})
82
})
83
.post('/api/auth/logout', async (c) => {
84
try {
···
86
const did = cookieSession.did?.value
87
88
// Clear the session cookie
89
+
cookieSession.did.remove()
90
91
// If we have a DID, try to revoke the OAuth session
92
if (did && typeof did === 'string') {
···
104
logger.error('[Auth] Logout error', err)
105
return { error: 'Logout failed' }
106
}
107
+
}, {
108
+
cookie: t.Cookie({
109
+
did: t.Optional(t.String())
110
+
}, {
111
+
secrets: cookieSecret,
112
+
sign: ['did']
113
+
})
114
})
115
.get('/api/auth/status', async (c) => {
116
try {
···
128
logger.error('[Auth] Status check error', err)
129
return { authenticated: false }
130
}
131
+
}, {
132
+
cookie: t.Cookie({
133
+
did: t.Optional(t.String())
134
+
}, {
135
+
secrets: cookieSecret,
136
+
sign: ['did']
137
+
})
138
})