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